YouTube 超快聊天

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

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

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