YouTube 超快聊天

让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。

当前为 2023-07-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @name:ja YouTube スーパーファーストチャット
  4. // @name:zh-TW YouTube 超快聊天
  5. // @name:zh-CN YouTube 超快聊天
  6. // @namespace UserScript
  7. // @match https://www.youtube.com/live_chat*
  8. // @version 0.1.4
  9. // @license MIT
  10. // @author CY Fung
  11. // @run-at document-start
  12. // @grant none
  13. // @unwrap
  14. // @allFrames true
  15. // @inject-into page
  16. //
  17. // @description To make your YouTube Live Chat scroll instantly without smoothing transform CSS
  18. // @description:ja YouTubeライブチャットをスムーズな変形CSSなしで瞬時にスクロールさせるために。
  19. // @description:zh-TW 讓您的 YouTube 直播聊天即時滾動,不經過平滑轉換 CSS。
  20. // @description:zh-CN 让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。
  21. //
  22. // ==/UserScript==
  23.  
  24. ((__CONTEXT__) => {
  25.  
  26. const ACTIVE_DEFERRED_APPEND = false; // somehow buggy
  27.  
  28. const addCss = () => document.head.appendChild(document.createElement('style')).textContent = `
  29.  
  30. @supports (contain: layout paint style) and (content-visibility: auto) and (contain-intrinsic-size: auto var(--wsr94)) {
  31.  
  32. [wSr93] {
  33. content-visibility: visible;
  34. }
  35.  
  36. [wSr93="hidden"]:nth-last-child(n+4) {
  37. content-visibility: auto;
  38. contain-intrinsic-size: auto var(--wsr94);
  39. }
  40.  
  41. }
  42.  
  43. @supports (contain: layout paint style) {
  44.  
  45.  
  46. /* optional */
  47. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  48. height: auto !important;
  49. min-height: unset !important;
  50. }
  51.  
  52. #items.style-scope.yt-live-chat-item-list-renderer {
  53. transform: translateY(0px) !important;
  54. /*padding-bottom: 0 !important;
  55. padding-top: 0 !important;*/
  56. }
  57.  
  58. /* optional */
  59.  
  60. yt-icon[icon="down_arrow"] > *,
  61. yt-icon-button#show-more > * {
  62. pointer-events: none !important;
  63. }
  64.  
  65.  
  66. #item-list.style-scope.yt-live-chat-renderer,
  67. yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer,
  68. #item-list.style-scope.yt-live-chat-renderer *,
  69. yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer * {
  70. will-change: unset !important;
  71. }
  72.  
  73. yt-img-shadow[height][width] {
  74. content-visibility: visible !important;
  75. }
  76.  
  77.  
  78. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
  79. position: static !important;
  80. }
  81.  
  82.  
  83. /* ------------------------------------------------------------------------------------------------------------- */
  84.  
  85. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
  86. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
  87. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
  88. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  89. contain: layout style;
  90. }
  91.  
  92. /*
  93. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
  94. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
  95. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
  96. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  97. contain: layout style;
  98. display: inline-flex;
  99. vertical-align: middle;
  100. }
  101. */
  102.  
  103. #items yt-live-chat-text-message-renderer {
  104. contain: layout style;
  105. }
  106.  
  107. yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
  108. overflow-y: scroll;
  109. padding-right: 0;
  110. }
  111.  
  112. body yt-live-chat-app {
  113. contain: size layout paint style;
  114. overflow: hidden;
  115. }
  116.  
  117. #items.style-scope.yt-live-chat-item-list-renderer {
  118. contain: layout paint style;
  119. }
  120.  
  121. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  122. contain: style;
  123. }
  124.  
  125. #item-scroller.style-scope.yt-live-chat-item-list-renderer {
  126. contain: size style;
  127. }
  128.  
  129. #contents.style-scope.yt-live-chat-item-list-renderer,
  130. #chat.style-scope.yt-live-chat-renderer,
  131. img.style-scope.yt-img-shadow[width][height] {
  132. contain: size layout paint style;
  133. }
  134.  
  135. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label],
  136. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
  137. contain: layout paint style;
  138. }
  139.  
  140. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer,
  141. yt-live-chat-membership-item-renderer.style-scope.yt-live-chat-item-list-renderer,
  142. yt-live-chat-paid-message-renderer.style-scope.yt-live-chat-item-list-renderer,
  143. yt-live-chat-banner-manager.style-scope.yt-live-chat-item-list-renderer {
  144. contain: layout style;
  145. }
  146.  
  147. tp-yt-paper-tooltip[style*="inset"][role="tooltip"] {
  148. contain: layout paint style;
  149. }
  150.  
  151. /*
  152. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  153. position: relative !important;
  154. height: auto !important;
  155. }
  156. */
  157.  
  158. /* ------------------------------------------------------------------------------------------------------------- */
  159.  
  160.  
  161. #items.style-scope.yt-live-chat-item-list-renderer {
  162. padding-top: var(--items-top-padding);
  163. }
  164.  
  165.  
  166. #continuations, #continuations * {
  167. contain: strict;
  168. position: fixed;
  169. top: 2px;
  170. height: 1px;
  171. width: 2px;
  172. height: 1px;
  173. visibility: collapse;
  174. }
  175.  
  176.  
  177. }
  178.  
  179. `;
  180.  
  181. const { Promise, requestAnimationFrame } = __CONTEXT__;
  182.  
  183.  
  184. const isContainSupport = CSS.supports('contain', 'layout paint style');
  185. if (!isContainSupport) {
  186. console.error(`
  187. YouTube Light Chat Scroll: Your browser does not support 'contain'.
  188. Chrome >= 52; Edge >= 79; Safari >= 15.4, Firefox >= 69; Opera >= 39
  189. `.trim());
  190. return;
  191. }
  192.  
  193. // const APPLY_delayAppendChild = false;
  194.  
  195. let activeDeferredAppendChild = false;
  196.  
  197. let delayedAppendParentWS = new WeakSet();
  198. let delayedAppendOperations = [];
  199. let commonAppendParentStackSet = new Set();
  200.  
  201. const sp7 = Symbol();
  202.  
  203.  
  204. let dt0 = Date.now() - 2000;
  205. const dateNow = () => Date.now() - dt0;
  206. let lastScroll = 0;
  207. let lastLShow = 0;
  208. let lastWheel = 0;
  209.  
  210. const proxyHelperFn = (dummy) => ({
  211.  
  212. get(target, prop) {
  213. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  214. },
  215. set(target, prop, value) {
  216. if (!(prop in dummy)) {
  217. target[prop] = value;
  218. }
  219. return true;
  220.  
  221. },
  222. has(target, prop) {
  223. return (prop in target)
  224. },
  225. deleteProperty(target, prop) {
  226.  
  227. return true;
  228. },
  229. ownKeys(target) {
  230. return Object.keys(target);
  231. },
  232. defineProperty(target, key, descriptor) {
  233. return Object.defineProperty(target, key, descriptor);
  234. // return true;
  235. },
  236. getOwnPropertyDescriptor(target, key) {
  237. return Object.getOwnPropertyDescriptor(target, key);
  238. },
  239.  
  240.  
  241.  
  242. });
  243.  
  244.  
  245. // const dummy3v = {
  246. // "background": "",
  247. // "backgroundAttachment": "",
  248. // "backgroundBlendMode": "",
  249. // "backgroundClip": "",
  250. // "backgroundColor": "",
  251. // "backgroundImage": "",
  252. // "backgroundOrigin": "",
  253. // "backgroundPosition": "",
  254. // "backgroundPositionX": "",
  255. // "backgroundPositionY": "",
  256. // "backgroundRepeat": "",
  257. // "backgroundRepeatX": "",
  258. // "backgroundRepeatY": "",
  259. // "backgroundSize": ""
  260. // };
  261. // for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  262. // dummy3v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  263. // }
  264.  
  265. // const dummy3p = phFn(dummy3v);
  266.  
  267. const pt2DecimalFixer = (x) => Math.round(x * 5, 0) / 5;
  268.  
  269. const tickerContainerSetAttribute = function (attrName, attrValue) {
  270.  
  271. let yd = (this.__dataHost || 0).__data;
  272.  
  273. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  274.  
  275. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  276. let v = `${attrValue}`;
  277. // conside a ticker is 101px width
  278. // 1% = 1.01px
  279. // 0.2% = 0.202px
  280.  
  281. const ratio1 = (yd.ratio * 100);
  282. const ratio2 = pt2DecimalFixer(ratio1);
  283. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  284.  
  285. if (yd.__style_last__ === v) return;
  286. yd.__style_last__ = v;
  287.  
  288. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  289.  
  290.  
  291.  
  292. } else {
  293. HTMLElement.prototype.setAttribute.apply(this, arguments);
  294. }
  295.  
  296. };
  297.  
  298.  
  299. /*
  300. *
  301. * const tickerContainerSetAttribute = function (attrName, attrValue) {
  302.  
  303. const yd = (this.__dataHost||0).__data;
  304. if (arguments.length === 2 && attrName === 'style' && attrValue && yd){
  305. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  306. let v = attrValue;
  307.  
  308. // conside a ticker is 101px width
  309. // 1% = 1.01px
  310. // 0.2% = 0.202px
  311. const ratio1 = (yd.ratio * 100);
  312. const ratio2 = pt2DecimalFixer(ratio1);
  313. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  314.  
  315. console.log(ratio1, ratio2)
  316. if (yd.__style_last__ !== v) {
  317. yd.__style_last__ = v; // clear along with data change
  318.  
  319. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  320. return;
  321. }
  322.  
  323.  
  324. }
  325. return HTMLElement.prototype.setAttribute.apply(this, arguments);
  326.  
  327. };
  328.  
  329. */
  330.  
  331.  
  332. const createDelayAppendOper = () => requestAnimationFrame(() => {
  333. const e = [...delayedAppendOperations]
  334. delayedAppendOperations.length = 0;
  335. for (const t of e) t();
  336. });
  337.  
  338. Node.prototype.appendChild = ((appendChild) => (function (s) {
  339. if (arguments.length !== 1) return appendChild.apply(this, arguments);
  340. // console.log(34, 1, this.is, this.nodeName, activeDeferredAppendChild, s.nodeName)
  341. const stack = new Error().stack;
  342.  
  343. if (ACTIVE_DEFERRED_APPEND && activeDeferredAppendChild && (commonAppendParentStackSet.has(stack) || s.nodeName === 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER') && typeof s.is === 'string') {
  344.  
  345. commonAppendParentStackSet.add(stack);
  346. // this = '#document-fragment'
  347. /*
  348. if (this instanceof HTMLElement) {
  349.  
  350.  
  351. if (ops.length === 0) createRAF();
  352. ops.push(() => {
  353. appendChild.apply(this, arguments);
  354. })
  355. return s;
  356.  
  357. } else {
  358.  
  359. mpws.add(this);
  360. appendChild.apply(this, arguments);
  361. return s;
  362. }
  363. */
  364. delayedAppendParentWS.add(this);
  365. if (delayedAppendOperations.length === 0) createDelayAppendOper();
  366. delayedAppendOperations.push(() => {
  367. delayedAppendParentWS.delete(this);
  368. appendChild.apply(this, arguments);
  369. })
  370. return s;
  371.  
  372. } else if (ACTIVE_DEFERRED_APPEND && activeDeferredAppendChild && delayedAppendParentWS.has(s)) {
  373.  
  374. /*
  375. if (this instanceof HTMLElement) {
  376. if (ops.length === 0) createRAF();
  377. ops.push(() => {
  378. mpws.delete(s);
  379. appendChild.apply(this, arguments);
  380. })
  381. return s;
  382. } else {
  383.  
  384. mpws.delete(s);
  385. appendChild.apply(this, arguments);
  386. return s;
  387. }
  388. */
  389.  
  390. if (delayedAppendOperations.length === 0) createDelayAppendOper();
  391. delayedAppendOperations.push(() => {
  392. delayedAppendParentWS.delete(s);
  393. appendChild.apply(this, arguments);
  394. })
  395. return s;
  396. } else if (this.nodeName === 'YT-LIVE-CHAT-TICKER-PAID-MESSAGE-ITEM-RENDERER') {
  397.  
  398.  
  399.  
  400. appendChild.call(this, s);
  401.  
  402. let container = this.$.container;
  403. if (container) {
  404.  
  405. // const sp3v = new Proxy(container.style, dummy3p)
  406.  
  407. // Object.defineProperty(container, 'style', {get(){return sp3v}, set() { }, enumerable: true, configurable: true });
  408.  
  409.  
  410. container.setAttribute = tickerContainerSetAttribute;
  411.  
  412.  
  413. }
  414.  
  415. return s;
  416. }
  417. // if(activeDeferredAppendChild) return null;
  418. appendChild.call(this, s);
  419. return s;
  420. }))(Node.prototype.appendChild);
  421.  
  422. /*
  423. Node.prototype.append = ((append) => (function () {
  424. // console.log(34,2 )
  425. return append.apply(this, arguments);
  426. }))(Node.prototype.append);
  427.  
  428. Node.prototype.insertBefore = ((insertBefore) => (function () {
  429. // console.log(34,3, this.is, this.nodeName, activeDeferredAppendChild)
  430. // if(activeDeferredAppendChild) return null;
  431. return insertBefore.apply(this, arguments);
  432. }))(Node.prototype.insertBefore);
  433.  
  434. Node.prototype.insertAfter = ((insertAfter) => (function () {
  435. // console.log(34,4)
  436. return insertAfter.apply(this, arguments);
  437. }))(Node.prototype.insertAfter);
  438.  
  439. */
  440.  
  441.  
  442.  
  443.  
  444. const fxOperator = (proto, propertyName) => {
  445. let propertyDescriptorGetter = null;
  446. try {
  447. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  448. } catch (e) { }
  449. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  450. };
  451.  
  452. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  453. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  454. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  455. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  456. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  457.  
  458.  
  459. /* globals WeakRef:false */
  460.  
  461. /** @type {(o: Object | null) => WeakRef | null} */
  462. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  463.  
  464. /** @type {(wr: Object | null) => Object | null} */
  465. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  466.  
  467. const watchUserCSS = () => {
  468.  
  469. if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  470.  
  471.  
  472. const clearContentVisibilitySizing = () => {
  473. Promise.resolve().then(() => {
  474.  
  475. for (const elm of document.querySelectorAll('[wSr93]')) {
  476. elm.setAttribute('wSr93', '');
  477. }
  478.  
  479.  
  480. })
  481.  
  482.  
  483. }
  484. const mutObserver = new MutationObserver((mutations) => {
  485. for (const mutation of mutations) {
  486. if ((mutation.addedNodes || 0).length >= 1) {
  487.  
  488. for (const addedNode of mutation.addedNodes) {
  489. if (addedNode.nodeName === 'SCRIPT') {
  490. clearContentVisibilitySizing();
  491. return;
  492. }
  493.  
  494. }
  495. }
  496. if ((mutation.remove || 0).length >= 1) {
  497.  
  498. for (const removedNode of mutation.removedNodes) {
  499.  
  500. if (removedNode.nodeName === 'SCRIPT') {
  501. clearContentVisibilitySizing();
  502. return;
  503. }
  504.  
  505. }
  506. }
  507. }
  508. });
  509. mutObserver.observe(document.documentElement, {
  510. childList: true,
  511. subtree: false
  512. })
  513.  
  514. mutObserver.observe(document.head, {
  515. childList: true,
  516. subtree: false
  517. })
  518. mutObserver.observe(document.body, {
  519. childList: true,
  520. subtree: false
  521. });
  522.  
  523.  
  524. }
  525.  
  526. let done = 0;
  527. let main = async (q) => {
  528.  
  529. if (done) return;
  530.  
  531. if (!q) return;
  532. let m1 = nodeParent(q);
  533. let m2 = q;
  534. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  535.  
  536. done = 1;
  537.  
  538. Promise.resolve().then(watchUserCSS);
  539.  
  540. addCss();
  541.  
  542. const dummy1v = {
  543. transform: '',
  544. height: '',
  545. minHeight: '',
  546. paddingBottom: '',
  547. paddingTop: ''
  548. };
  549. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  550. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  551. }
  552.  
  553.  
  554.  
  555. const dummy1p = proxyHelperFn(dummy1v);
  556. const sp1v = new Proxy(m1.style, dummy1p);
  557. const sp2v = new Proxy(m2.style, dummy1p);
  558. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  559. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  560. m1.removeAttribute("style");
  561. m2.removeAttribute("style");
  562.  
  563. let lastClick = 0;
  564. document.addEventListener('click', (evt) => {
  565. if (!evt.isTrusted) return;
  566. const target = ((evt || 0).target || 0)
  567. if (target.id === 'show-more') {
  568. if (target.nodeName !== 'YT-ICON-BUTTON') return;
  569.  
  570. if (dateNow() - lastClick < 80) return;
  571. requestAnimationFrame(() => {
  572. lastClick = dateNow();
  573. target.click();
  574. })
  575. }
  576.  
  577. })
  578.  
  579. let btnShowMoreWR = null;
  580.  
  581.  
  582. const clickShowMore = () => {
  583. let btnShowMore = kRef(btnShowMoreWR);
  584. if (!btnShowMore || !btnShowMore.isConnected) {
  585. btnShowMore = document.querySelector('#show-more.yt-live-chat-item-list-renderer');
  586. btnShowMoreWR = mWeakRef(btnShowMore);
  587. }
  588. if (btnShowMore) btnShowMore.click();
  589. };
  590.  
  591. let hasFirstShowMore = false;
  592.  
  593. const visObserver = new IntersectionObserver((entries) => {
  594.  
  595. for (const entry of entries) {
  596.  
  597. const target = entry.target;
  598. if (!target) continue;
  599. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  600. if (isVisible) {
  601. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  602. target.setAttribute('wSr93', 'visible');
  603. if (nNextElem(target) === null) {
  604. canSetMaxScrollTop = true;
  605. if (dateNow() - lastScroll < 80) {
  606. lastLShow = 0;
  607. lastScroll = 0;
  608. Promise.resolve().then(clickShowMore);
  609. } else {
  610. lastLShow = dateNow();
  611. }
  612. } else if (!hasFirstShowMore) { // should more than one item being visible
  613. // implement inside visObserver to ensure there is sufficient delay
  614. hasFirstShowMore = true;
  615. requestAnimationFrame(() => {
  616. // foreground page
  617. activeDeferredAppendChild = true;
  618. // page visibly ready -> load the latest comments at initial loading
  619. clickShowMore();
  620. });
  621. }
  622. }
  623. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  624.  
  625. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  626. target.setAttribute('wSr93', 'hidden');
  627. }
  628.  
  629. }
  630.  
  631. }, {
  632. /*
  633. root: items,
  634. rootMargin: "0px",
  635. threshold: 1.0,
  636. */
  637. root: document.querySelector('#item-scroller'), // nullable
  638. rootMargin: "0px",
  639. threshold: [0.05, 0.95],
  640. });
  641.  
  642. //m2.style.visibility='';
  643.  
  644. const mutFn = (items) => {
  645. let node = nLastElem(items);
  646. for (; node !== null; node = nPrevElem(node)) {
  647. if (node.hasAttribute('wSr93')) break;
  648. node.setAttribute('wSr93', '');
  649. visObserver.observe(node);
  650. }
  651. }
  652.  
  653. const mutObserver = new MutationObserver((mutations) => {
  654. const items = (mutations[0] || 0).target;
  655. if (!items) return;
  656. mutFn(items);
  657. });
  658. mutObserver.observe(m2, {
  659. childList: true,
  660. subtree: false
  661. });
  662. mutFn(m2);
  663.  
  664.  
  665. /** @type {HTMLElement} */
  666. let c1 = nPrevElem(m1);
  667. if (c1 && c1.id === "live-chat-banner") {
  668. let rsObserver = new ResizeObserver((entries) => {
  669.  
  670. for (const entry of entries) {
  671. const target = entry.target;
  672. if (target && target.id === "live-chat-banner") {
  673. let p = entry.borderBoxSize ? (entry.borderBoxSize[0] || 0).blockSize : 0;
  674. let c1h = p > entry.contentRect.height ? p : entry.contentRect.height + 16;
  675. document.documentElement.style.setProperty('--items-top-padding', (Math.ceil(c1h / 2) * 2) + 'px');
  676. }
  677. }
  678.  
  679. });
  680. rsObserver.observe(c1);
  681. }
  682.  
  683. let maxScrollTop = -1;
  684. let canSetMaxScrollTop = false;
  685. document.addEventListener('scroll', (evt) => {
  686.  
  687. if (!evt || !evt.isTrusted) return;
  688. if (!canSetMaxScrollTop) return;
  689. const isUserAction = dateNow() - lastWheel<80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  690. if(!isUserAction) return;
  691.  
  692. if (dateNow() - lastLShow < 80) {
  693. lastLShow = 0;
  694. lastScroll = 0;
  695. Promise.resolve().then(clickShowMore);
  696. } else {
  697. lastScroll = dateNow();
  698. }
  699.  
  700.  
  701.  
  702. }, { passive: true, capture: true }) // support contain => support passive
  703.  
  704.  
  705. document.addEventListener('wheel', (evt) => {
  706.  
  707. if (!evt || !evt.isTrusted) return;
  708. lastWheel = dateNow();
  709.  
  710.  
  711. }, { passive: true, capture: true }) // support contain => support passive
  712.  
  713. };
  714.  
  715.  
  716.  
  717. function onReady() {
  718. let tmObserver = new MutationObserver(() => {
  719.  
  720. let p = document.getElementById('items'); // fast
  721. if (!p) return;
  722. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  723.  
  724. if (q) {
  725. tmObserver.disconnect();
  726. tmObserver.takeRecords();
  727. tmObserver = null;
  728. Promise.resolve(q).then((q) => {
  729. // confirm Promis.resolve() is resolveable
  730. // execute main without direct blocking
  731. main(q);
  732. })
  733. }
  734.  
  735. });
  736.  
  737. tmObserver.observe(document.body, {
  738. childList: true,
  739. subtree: true
  740. });
  741.  
  742. }
  743.  
  744.  
  745.  
  746. if (document.readyState != 'loading') {
  747. onReady();
  748. } else {
  749. window.addEventListener("DOMContentLoaded", onReady, false);
  750. }
  751.  
  752.  
  753. })({ Promise, requestAnimationFrame });