YouTube 超快聊天

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

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

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