YouTube 超快聊天

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

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

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @version 0.5.9
  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. yt-live-chat-renderer[has-action-panel-renderer] #show-more.yt-live-chat-item-list-renderer{
  173. top: 4px;
  174. transition-property: top;
  175. bottom: unset;
  176. }
  177.  
  178. yt-live-chat-renderer[has-action-panel-renderer] #show-more.yt-live-chat-item-list-renderer[disabled]{
  179. top: -42px;
  180. }
  181.  
  182. }
  183.  
  184. `;
  185.  
  186. const { Promise, requestAnimationFrame, IntersectionObserver } = __CONTEXT__;
  187.  
  188.  
  189. const isContainSupport = CSS.supports('contain', 'layout paint style');
  190. if (!isContainSupport) {
  191. console.error(`
  192. YouTube Light Chat Scroll: Your browser does not support 'contain'.
  193. Chrome >= 52; Edge >= 79; Safari >= 15.4, Firefox >= 69; Opera >= 39
  194. `.trim());
  195. return;
  196. }
  197.  
  198. // const APPLY_delayAppendChild = false;
  199.  
  200. // let activeDeferredAppendChild = false; // deprecated
  201.  
  202. // let delayedAppendParentWS = new WeakSet();
  203. // let delayedAppendOperations = [];
  204. // let commonAppendParentStackSet = new Set();
  205.  
  206. // let firstVisibleItemDetected = false; // deprecated
  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. has(target, prop) {
  229. return (prop in target)
  230. },
  231. deleteProperty(target, prop) {
  232. return true;
  233. },
  234. ownKeys(target) {
  235. return Object.keys(target);
  236. },
  237. defineProperty(target, key, descriptor) {
  238. return Object.defineProperty(target, key, descriptor);
  239. },
  240. getOwnPropertyDescriptor(target, key) {
  241. return Object.getOwnPropertyDescriptor(target, key);
  242. },
  243.  
  244. });
  245.  
  246. const tickerContainerSetAttribute = function (attrName, attrValue) { // ensure '14.30000001%'.toFixed(1)
  247.  
  248. let yd = (this.__dataHost || (this.inst || 0).__dataHost).__data;
  249.  
  250. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  251.  
  252. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  253. let v = `${attrValue}`;
  254. // conside a ticker is 101px width
  255. // 1% = 1.01px
  256. // 0.2% = 0.202px
  257.  
  258.  
  259. const ratio1 = (yd.ratio * 100);
  260. if (ratio1 > -1) { // avoid NaN
  261.  
  262. const ratio2 = ratio1.toFixed(1)
  263. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  264.  
  265. if (yd.__style_last__ === v) return;
  266. yd.__style_last__ = v;
  267. // do not consider any delay here.
  268. // it shall be inside the looping for all properties changes. all the css background ops are in the same microtask.
  269.  
  270. }
  271.  
  272. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  273.  
  274.  
  275. } else {
  276. HTMLElement.prototype.setAttribute.apply(this, arguments);
  277. }
  278.  
  279. };
  280.  
  281. const fxOperator = (proto, propertyName) => {
  282. let propertyDescriptorGetter = null;
  283. try {
  284. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  285. } catch (e) { }
  286. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  287. };
  288.  
  289. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  290. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  291. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  292. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  293. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  294.  
  295.  
  296. /* globals WeakRef:false */
  297.  
  298. /** @type {(o: Object | null) => WeakRef | null} */
  299. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  300.  
  301. /** @type {(wr: Object | null) => Object | null} */
  302. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  303.  
  304. const watchUserCSS = () => {
  305.  
  306. // if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  307.  
  308. const getElemFromWR = (nr) => {
  309. const n = kRef(nr);
  310. if (n && n.isConnected) return n;
  311. return null;
  312. }
  313.  
  314. const clearContentVisibilitySizing = () => {
  315. Promise.resolve().then(() => {
  316.  
  317. let btnShowMoreWR = mWeakRef(document.querySelector('#show-more[disabled]'));
  318.  
  319. let lastVisibleItemWR = null;
  320. for (const elm of document.querySelectorAll('[wSr93]')) {
  321. if (elm.getAttribute('wSr93') === 'visible') lastVisibleItemWR = mWeakRef(elm);
  322. elm.setAttribute('wSr93', '');
  323. // custom CSS property --wsr94 not working when attribute wSr93 removed
  324. }
  325. requestAnimationFrame(() => {
  326. const btnShowMore = getElemFromWR(btnShowMoreWR); btnShowMoreWR = null;
  327. if (btnShowMore) btnShowMore.click();
  328. else {
  329. // would not work if switch it frequently
  330. const lastVisibleItem = getElemFromWR(lastVisibleItemWR); lastVisibleItemWR = null;
  331. if (lastVisibleItem) {
  332.  
  333. Promise.resolve()
  334. .then(() => lastVisibleItem.scrollIntoView())
  335. .then(() => lastVisibleItem.scrollIntoView(false))
  336. .then(() => lastVisibleItem.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }))
  337. .catch(e => { }) // break the chain when method not callable
  338.  
  339. }
  340. }
  341. })
  342.  
  343. })
  344.  
  345. }
  346.  
  347. const mutObserver = new MutationObserver((mutations) => {
  348. for (const mutation of mutations) {
  349. if ((mutation.addedNodes || 0).length >= 1) {
  350. for (const addedNode of mutation.addedNodes) {
  351. if (addedNode.nodeName === 'STYLE') {
  352. clearContentVisibilitySizing();
  353. return;
  354. }
  355. }
  356. }
  357. if ((mutation.removedNodes || 0).length >= 1) {
  358. for (const removedNode of mutation.removedNodes) {
  359. if (removedNode.nodeName === 'STYLE') {
  360. clearContentVisibilitySizing();
  361. return;
  362. }
  363. }
  364. }
  365. }
  366. });
  367.  
  368. mutObserver.observe(document.documentElement, {
  369. childList: true,
  370. subtree: false
  371. })
  372.  
  373. mutObserver.observe(document.head, {
  374. childList: true,
  375. subtree: false
  376. })
  377. mutObserver.observe(document.body, {
  378. childList: true,
  379. subtree: false
  380. });
  381.  
  382. }
  383.  
  384. const setupStyle = (m1, m2) => {
  385.  
  386. const dummy1v = {
  387. transform: '',
  388. height: '',
  389. minHeight: '',
  390. paddingBottom: '',
  391. paddingTop: ''
  392. };
  393. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  394. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  395. }
  396.  
  397. const dummy1p = proxyHelperFn(dummy1v);
  398. const sp1v = new Proxy(m1.style, dummy1p);
  399. const sp2v = new Proxy(m2.style, dummy1p);
  400. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  401. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  402. m1.removeAttribute("style");
  403. m2.removeAttribute("style");
  404.  
  405. }
  406.  
  407.  
  408. class WillChangeController {
  409. constructor(itemScroller, willChangeValue) {
  410. this.element = itemScroller;
  411. this.counter = 0;
  412. this.active = false;
  413. this.willChangeValue = willChangeValue;
  414. }
  415.  
  416. beforeOper() {
  417. if (!this.active) {
  418. this.active = true;
  419. this.element.style.willChange = this.willChangeValue;
  420. }
  421. this.counter++;
  422. }
  423.  
  424. afterOper() {
  425. const c = this.counter;
  426. requestAnimationFrame(() => {
  427. if (c === this.counter) {
  428. this.active = false;
  429. this.element.style.willChange = '';
  430. }
  431. })
  432. }
  433.  
  434. release() {
  435. const element = this.element;
  436. this.element = null;
  437. this.counter = 1e16;
  438. this.active = false;
  439. try {
  440. element.style.willChange = '';
  441. } catch (e) { }
  442. }
  443.  
  444. }
  445.  
  446.  
  447. customYtElements.onRegistryReady(() => {
  448.  
  449. let scrollWillChangeController = null;
  450. let contensWillChangeController = null;
  451.  
  452. // as it links to event handling, it has to be injected using immediateCallback
  453. customYtElements.whenRegistered('yt-live-chat-item-list-renderer', (cProto) => {
  454.  
  455. const mclp = cProto;
  456. console.assert(typeof mclp.scrollToBottom_ === 'function')
  457. console.assert(typeof mclp.scrollToBottom66_ !== 'function')
  458. console.assert(typeof mclp.flushActiveItems_ === 'function')
  459. console.assert(typeof mclp.flushActiveItems66_ !== 'function')
  460.  
  461.  
  462. mclp.__intermediate_delay__ = null;
  463.  
  464. mclp.scrollToBottom66_ = mclp.scrollToBottom_;
  465. mclp.scrollToBottom_ = function () {
  466. const cnt = this;
  467. const itemScroller = cnt.itemScroller;
  468. if (scrollWillChangeController && scrollWillChangeController.element !== itemScroller) {
  469. scrollWillChangeController.release();
  470. scrollWillChangeController = null;
  471. }
  472. if (!scrollWillChangeController) scrollWillChangeController = new WillChangeController(itemScroller, 'scroll-position');
  473. const wcController = scrollWillChangeController;
  474. wcController.beforeOper();
  475. cnt.__intermediate_delay__ = new Promise(resolve => {
  476. Promise.resolve().then(() => {
  477. cnt.scrollToBottom66_()
  478. resolve();
  479. }).then(() => {
  480. wcController.afterOper();
  481. });
  482. });
  483. }
  484.  
  485. mclp.flushActiveItems66_ = mclp.flushActiveItems_;
  486. mclp.flushActiveItems_ = function () {
  487. const cnt = this;
  488.  
  489. if (arguments.length !== 0) return cnt.flushActiveItems66_.apply(this, arguments);
  490.  
  491. if (cnt.activeItems_.length === 0) {
  492. cnt.__intermediate_delay__ = null;
  493. return;
  494. }
  495.  
  496. const items = (cnt.$ || 0).items;
  497. if (contensWillChangeController && contensWillChangeController.element !== items) {
  498. contensWillChangeController.release();
  499. contensWillChangeController = null;
  500. }
  501. if (!contensWillChangeController) contensWillChangeController = new WillChangeController(items, 'contents');
  502. const wcController = contensWillChangeController;
  503.  
  504. // ignore previous __intermediate_delay__ and create a new one
  505. cnt.__intermediate_delay__ = new Promise(resolve => {
  506. if (cnt.activeItems_.length === 0) {
  507. resolve();
  508. } else {
  509. if (cnt.canScrollToBottom_()) {
  510. wcController.beforeOper();
  511. Promise.resolve().then(() => {
  512. cnt.flushActiveItems66_();
  513. resolve();
  514. }).then(() => {
  515. cnt.async(() => {
  516. wcController.afterOper();
  517. resolve();
  518. });
  519. })
  520. } else {
  521. Promise.resolve().then(() => {
  522. cnt.flushActiveItems66_();
  523. resolve();
  524. })
  525. }
  526. }
  527. });
  528.  
  529. }
  530.  
  531. mclp.async66 = mclp.async;
  532. mclp.async = function () {
  533. // ensure the previous operation is done
  534. // .async is usually after the time consuming functions like flushActiveItems_ and scrollToBottom_
  535.  
  536. (this.__intermediate_delay__ || Promise.resolve()).then(() => {
  537. this.async66.apply(this, arguments);
  538. });
  539.  
  540. }
  541.  
  542. })
  543.  
  544. });
  545.  
  546. const getProto = (element) => {
  547. let proto = null;
  548. if (element) {
  549. if (element.inst) proto = element.inst.constructor.prototype;
  550. else proto = element.constructor.prototype;
  551. }
  552. return proto || null;
  553. }
  554.  
  555. let done = 0;
  556. let main = async (q) => {
  557.  
  558. if (done) return;
  559.  
  560. if (!q) return;
  561. let m1 = nodeParent(q);
  562. let m2 = q;
  563. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  564.  
  565. done = 1;
  566.  
  567. Promise.resolve().then(watchUserCSS);
  568.  
  569. addCss();
  570.  
  571. setupStyle(m1, m2);
  572.  
  573. let lcRendererWR = null;
  574.  
  575. const lcRendererElm = () => {
  576. let lcRenderer = kRef(lcRendererWR);
  577. if (!lcRenderer || !lcRenderer.isConnected) {
  578. lcRenderer = document.querySelector('yt-live-chat-item-list-renderer.yt-live-chat-renderer');
  579. lcRendererWR = lcRenderer ? mWeakRef(lcRenderer) : null;
  580. }
  581. return lcRenderer
  582. };
  583.  
  584. let hasFirstShowMore = false;
  585.  
  586. const visObserverFn = (entry) => {
  587.  
  588. const target = entry.target;
  589. if (!target) return;
  590. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  591. const h = entry.boundingClientRect.height;
  592. if (h < 16) { // wrong: 8 (padding/margin); standard: 32; test: 16 or 20
  593. // e.g. under fullscreen. the element created but not rendered.
  594. target.setAttribute('wSr93', '');
  595. return;
  596. }
  597. if (isVisible) {
  598. target.style.setProperty('--wsr94', h + 'px');
  599. target.setAttribute('wSr93', 'visible');
  600. if (nNextElem(target) === null) {
  601. // firstVisibleItemDetected = true;
  602. /*
  603. if (dateNow() - lastScroll < 80) {
  604. lastLShow = 0;
  605. lastScroll = 0;
  606. Promise.resolve().then(clickShowMore);
  607. } else {
  608. lastLShow = dateNow();
  609. }
  610. */
  611. // lastLShow = dateNow();
  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. const lcRenderer = lcRendererElm();
  620. if (lcRenderer) {
  621. (lcRenderer.inst || lcRenderer).scrollToBottom_();
  622. }
  623. });
  624. }
  625. }
  626. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  627.  
  628. target.style.setProperty('--wsr94', h + 'px');
  629. target.setAttribute('wSr93', 'hidden');
  630. } // note: might consider 0 < entry.intersectionRatio < 0.5 and target.getAttribute('wSr93') === '' <new last item>
  631.  
  632. }
  633.  
  634. const visObserver = new IntersectionObserver((entries) => {
  635.  
  636. for (const entry of entries) {
  637.  
  638. Promise.resolve(entry).then(visObserverFn);
  639.  
  640. }
  641.  
  642. }, {
  643. /*
  644. root: items,
  645. rootMargin: "0px",
  646. threshold: 1.0,
  647. */
  648. // root: HTMLElement.prototype.closest.call(m2, '#item-scroller.yt-live-chat-item-list-renderer'), // nullable
  649. rootMargin: "0px",
  650. threshold: [0.05, 0.95],
  651. });
  652.  
  653. //m2.style.visibility='';
  654.  
  655. const mutFn = (items) => {
  656. for (let node = nLastElem(items); node !== null; node = nPrevElem(node)) {
  657. if (node.hasAttribute('wSr93')) break;
  658. node.setAttribute('wSr93', '');
  659. visObserver.observe(node);
  660. }
  661. }
  662.  
  663. const mutObserver = new MutationObserver((mutations) => {
  664. const items = (mutations[0] || 0).target;
  665. if (!items) return;
  666. mutFn(items);
  667. });
  668.  
  669. const setupMutObserver = (m2) => {
  670. mutObserver.disconnect();
  671. mutObserver.takeRecords();
  672. if (m2) {
  673. mutObserver.observe(m2, {
  674. childList: true,
  675. subtree: false
  676. });
  677. mutFn(m2);
  678. }
  679. }
  680.  
  681. setupMutObserver(m2);
  682.  
  683. const mclp = getProto(document.querySelector('yt-live-chat-item-list-renderer'));
  684. if (mclp && mclp.attached) {
  685.  
  686. mclp.attached66 = mclp.attached;
  687. mclp.attached = function () {
  688. let m2 = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');
  689. let m1 = nodeParent(m2);
  690. setupStyle(m1, m2);
  691. setupMutObserver(m2);
  692. return this.attached66();
  693. }
  694.  
  695. mclp.detached66 = mclp.detached;
  696. mclp.detached = function () {
  697. setupMutObserver();
  698. return this.detached66();
  699. }
  700.  
  701. mclp.canScrollToBottom_ = function () {
  702. return this.atBottom && this.allowScroll && !(dateNow() - lastWheel < 80)
  703. }
  704.  
  705. mclp.isSmoothScrollEnabled_ = function () {
  706. return false;
  707. }
  708.  
  709. } else {
  710. console.warn(`proto.attached for yt-live-chat-item-list-renderer is unavailable.`)
  711. }
  712.  
  713.  
  714. let scrollCount = 0;
  715. document.addEventListener('scroll', (evt) => {
  716. if (!evt || !evt.isTrusted) return;
  717. // lastScroll = dateNow();
  718. if (++scrollCount > 1e9) scrollCount = 9;
  719. }, { passive: true, capture: true }) // support contain => support passive
  720.  
  721. // document.addEventListener('scroll', (evt) => {
  722.  
  723. // if (!evt || !evt.isTrusted) return;
  724. // if (!firstVisibleItemDetected) return;
  725. // const isUserAction = dateNow() - lastWheel < 80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  726. // if (!isUserAction) return;
  727. // // lastScroll = dateNow();
  728.  
  729. // }, { passive: true, capture: true }) // support contain => support passive
  730.  
  731.  
  732. let lastScrollCount = -1;
  733. document.addEventListener('wheel', (evt) => {
  734.  
  735. if (!evt || !evt.isTrusted) return;
  736. if (lastScrollCount === scrollCount) return;
  737. lastScrollCount = scrollCount;
  738. lastWheel = dateNow();
  739.  
  740. }, { passive: true, capture: true }) // support contain => support passive
  741.  
  742.  
  743. const fp = (renderer) => {
  744. const hostElement = renderer.inst || renderer;
  745. const container = (hostElement.$ || 0).container;
  746. if (container) {
  747. container.setAttribute = tickerContainerSetAttribute;
  748. }
  749. }
  750. const tags = ["yt-live-chat-ticker-paid-message-item-renderer", "yt-live-chat-ticker-paid-sticker-item-renderer",
  751. "yt-live-chat-ticker-renderer", "yt-live-chat-ticker-sponsor-item-renderer"];
  752. for (const tag of tags) {
  753. const dummy = document.createElement(tag);
  754.  
  755. const cProto = getProto(dummy);
  756. if (!cProto || !cProto.attached) {
  757. console.warn(`proto.attached for ${tag} is unavailable.`)
  758. continue;
  759. }
  760.  
  761. const __updateTimeout__ = cProto.updateTimeout;
  762.  
  763. const canDoUpdateTimeoutReplacement = (() => {
  764.  
  765. if (dummy.countdownMs < 1 && dummy.lastCountdownTimeMs < 1 && dummy.countdownMs < 1 && dummy.countdownDurationMs < 1) {
  766. return typeof dummy.setContainerWidth === 'function' && typeof dummy.slideDown === 'function';
  767. }
  768. return false;
  769.  
  770. })(dummy.inst || dummy) && ((__updateTimeout__ + "").indexOf("window.requestAnimationFrame(this.updateTimeout.bind(this))") > 0);
  771.  
  772.  
  773.  
  774. if (canDoUpdateTimeoutReplacement) {
  775.  
  776. const killTicker = (cnt) => {
  777. if ("auto" === cnt.hostElement.style.width) cnt.setContainerWidth();
  778. cnt.slideDown()
  779. };
  780.  
  781. cProto.__ratio__ = null;
  782. cProto._updateTimeout21_ = function (a) {
  783.  
  784. /*
  785. let pRatio = this.countdownMs / this.countdownDurationMs;
  786. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  787. let noMoreCountDown = this.countdownMs < 1e-6;
  788. let qRatio = this.countdownMs / this.countdownDurationMs;
  789. if(noMoreCountDown){
  790. this.countdownMs = 0;
  791. this.ratio = 0;
  792. } else if( pRatio - qRatio < 0.001 && qRatio < pRatio){
  793.  
  794. }else{
  795. this.ratio = qRatio;
  796. }
  797. */
  798.  
  799. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  800.  
  801. let currentRatio = this.__ratio__;
  802. let tdv = this.countdownMs / this.countdownDurationMs;
  803. let nextRatio = Math.round(tdv * 500) / 500; // might generate 0.143000000001
  804.  
  805. const validCountDown = nextRatio > 0;
  806. const isAttached = this.isAttached;
  807.  
  808. if (!validCountDown) {
  809.  
  810. this.lastCountdownTimeMs = null;
  811.  
  812. this.countdownMs = 0;
  813. this.__ratio__ = null;
  814. this.ratio = 0;
  815.  
  816. if (isAttached) Promise.resolve(this).then(killTicker);
  817.  
  818. } else if (!isAttached) {
  819.  
  820. this.lastCountdownTimeMs = null;
  821.  
  822. } else {
  823.  
  824. this.lastCountdownTimeMs = a;
  825.  
  826. const ratioDiff = currentRatio - nextRatio; // 0.144 - 0.142 = 0.002
  827. if (ratioDiff < 0.001 && ratioDiff > -1e-6) {
  828. // ratioDiff = 0
  829.  
  830. } else {
  831. // ratioDiff = 0.002 / 0.004 ....
  832. // OR ratioDiff < 0
  833.  
  834. this.__ratio__ = nextRatio;
  835.  
  836. this.ratio = nextRatio;
  837. }
  838.  
  839. return true;
  840. }
  841.  
  842. };
  843.  
  844. cProto.updateTimeout = async function (a) {
  845.  
  846. let ret = this._updateTimeout21_(a);
  847. while (ret) {
  848. let a = await new Promise(resolve => {
  849. this.rafId = requestAnimationFrame(resolve)
  850. }); // could be never resolve
  851. ret = this._updateTimeout21_(a);
  852. }
  853.  
  854. };
  855.  
  856. }
  857.  
  858. cProto.attached77 = cProto.attached
  859.  
  860. cProto.attached = function () {
  861. fp(this.hostElement || this);
  862. return this.attached77();
  863. }
  864.  
  865. for (const elm of document.getElementsByTagName(tag)) {
  866. fp(elm);
  867. }
  868.  
  869.  
  870. }
  871.  
  872. };
  873.  
  874.  
  875. function onReady() {
  876. let tmObserver = new MutationObserver(() => {
  877.  
  878. let p = document.getElementById('items'); // fast
  879. if (!p) return;
  880. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  881.  
  882. if (q) {
  883. tmObserver.disconnect();
  884. tmObserver.takeRecords();
  885. tmObserver = null;
  886. Promise.resolve(q).then((q) => {
  887. // confirm Promis.resolve() is resolveable
  888. // execute main without direct blocking
  889. main(q);
  890. })
  891. }
  892.  
  893. });
  894.  
  895. tmObserver.observe(document.body || document.documentElement, {
  896. childList: true,
  897. subtree: true
  898. });
  899.  
  900. }
  901.  
  902. Promise.resolve().then(() => {
  903.  
  904. if (document.readyState !== 'loading') {
  905. onReady();
  906. } else {
  907. window.addEventListener("DOMContentLoaded", onReady, false);
  908. }
  909.  
  910. });
  911.  
  912. })({ Promise, requestAnimationFrame, IntersectionObserver });