YouTube 超快聊天

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

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

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