YouTube 超快聊天

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

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

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @version 0.5.22
  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="hidden"] { /* initial->[wSr93]->[wSr93="visible"]->[wSr93="hidden"] => reliable rendered height */
  53. --wsr93-contain: size layout style;
  54. height: var(--wsr94);
  55. }
  56.  
  57.  
  58. /* ------------------------------------------------------------------------------------------------------------- */
  59.  
  60. 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 {
  61. contain: layout style;
  62. }
  63.  
  64. body yt-live-chat-app {
  65. contain: size layout paint style;
  66. overflow: hidden;
  67. }
  68.  
  69. #items.style-scope.yt-live-chat-item-list-renderer {
  70. contain: layout paint style;
  71. }
  72.  
  73. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  74. contain: style;
  75. }
  76.  
  77. #item-scroller.style-scope.yt-live-chat-item-list-renderer {
  78. contain: size style;
  79. }
  80.  
  81. #contents.style-scope.yt-live-chat-item-list-renderer, #chat.style-scope.yt-live-chat-renderer, img.style-scope.yt-img-shadow[width][height] {
  82. contain: size layout paint style;
  83. }
  84.  
  85. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label], .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
  86. contain: layout paint style;
  87. }
  88.  
  89. 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 {
  90. contain: layout style;
  91. }
  92.  
  93. tp-yt-paper-tooltip[style*="inset"][role="tooltip"] {
  94. contain: layout paint style;
  95. }
  96.  
  97. /* ------------------------------------------------------------------------------------------------------------- */
  98.  
  99. }
  100.  
  101. @supports (color: var(--general)) {
  102.  
  103. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
  104. position: static !important;
  105. }
  106.  
  107. .ytp-contextmenu[class],
  108. .toggle-button.tp-yt-paper-toggle-button[class],
  109. .yt-spec-touch-feedback-shape__fill[class],
  110. .fill.yt-interaction[class],
  111. .ytp-videowall-still-info-content[class],
  112. .ytp-suggestion-image[class] {
  113. will-change: unset !important;
  114. }
  115.  
  116. yt-img-shadow[height][width] {
  117. content-visibility: visible !important;
  118. }
  119.  
  120. yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
  121. overflow-y: scroll;
  122. padding-right: 0;
  123. }
  124.  
  125.  
  126. /* optional */
  127. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  128. height: auto !important;
  129. min-height: unset !important;
  130. }
  131.  
  132. #items.style-scope.yt-live-chat-item-list-renderer {
  133. transform: translateY(0px) !important;
  134. }
  135.  
  136. /* optional */
  137. yt-icon[icon="down_arrow"] > *, yt-icon-button#show-more > * {
  138. pointer-events: none !important;
  139. }
  140.  
  141. #continuations, #continuations * {
  142. contain: strict;
  143. position: fixed;
  144. top: 2px;
  145. height: 1px;
  146. width: 2px;
  147. height: 1px;
  148. visibility: collapse;
  149. }
  150.  
  151. yt-live-chat-renderer[has-action-panel-renderer] #show-more.yt-live-chat-item-list-renderer{
  152. top: 4px;
  153. transition-property: top;
  154. bottom: unset;
  155. }
  156.  
  157. yt-live-chat-renderer[has-action-panel-renderer] #show-more.yt-live-chat-item-list-renderer[disabled]{
  158. top: -42px;
  159. }
  160.  
  161. html #panel-pages.yt-live-chat-renderer > #input-panel.yt-live-chat-renderer:not(:empty) {
  162. --yt-live-chat-action-panel-top-border: none;
  163. }
  164.  
  165. html #panel-pages.yt-live-chat-renderer > #input-panel.yt-live-chat-renderer.iron-selected > *:first-child {
  166. border-top: 1px solid var(--yt-live-chat-panel-pages-border-color);
  167. }
  168.  
  169. html #panel-pages.yt-live-chat-renderer {
  170. border-top: 0;
  171. border-bottom: 0;
  172. }
  173.  
  174. #input-panel #picker-buttons yt-live-chat-icon-toggle-button-renderer#product-picker {
  175. overflow: hidden;
  176. contain: layout paint style;
  177. }
  178.  
  179. }
  180.  
  181. `;
  182.  
  183. const { Promise, requestAnimationFrame, IntersectionObserver } = __CONTEXT__;
  184.  
  185.  
  186. const isContainSupport = CSS.supports('contain', 'layout paint style');
  187. if (!isContainSupport) {
  188. console.warn("Your browser does not support css property 'contain'.\nPlease upgrade to the latest version.".trim());
  189. }
  190.  
  191. // const APPLY_delayAppendChild = false;
  192.  
  193. // let activeDeferredAppendChild = false; // deprecated
  194.  
  195. // let delayedAppendParentWS = new WeakSet();
  196. // let delayedAppendOperations = [];
  197. // let commonAppendParentStackSet = new Set();
  198.  
  199. // let firstVisibleItemDetected = false; // deprecated
  200.  
  201. const sp7 = Symbol();
  202.  
  203.  
  204. let dt0 = Date.now() - 2000;
  205. const dateNow = () => Date.now() - dt0;
  206. // let lastScroll = 0;
  207. // let lastLShow = 0;
  208. let lastWheel = 0;
  209.  
  210. const proxyHelperFn = (dummy) => ({
  211.  
  212. get(target, prop) {
  213. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  214. },
  215. set(target, prop, value) {
  216. if (!(prop in dummy)) {
  217. target[prop] = value;
  218. }
  219. return true;
  220. },
  221. has(target, prop) {
  222. return (prop in target)
  223. },
  224. deleteProperty(target, prop) {
  225. return true;
  226. },
  227. ownKeys(target) {
  228. return Object.keys(target);
  229. },
  230. defineProperty(target, key, descriptor) {
  231. return Object.defineProperty(target, key, descriptor);
  232. },
  233. getOwnPropertyDescriptor(target, key) {
  234. return Object.getOwnPropertyDescriptor(target, key);
  235. },
  236.  
  237. });
  238.  
  239. const tickerContainerSetAttribute = function (attrName, attrValue) { // ensure '14.30000001%'.toFixed(1)
  240.  
  241. let yd = (this.__dataHost || (this.inst || 0).__dataHost).__data;
  242.  
  243. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  244.  
  245. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  246. let v = `${attrValue}`;
  247. // conside a ticker is 101px width
  248. // 1% = 1.01px
  249. // 0.2% = 0.202px
  250.  
  251.  
  252. const ratio1 = (yd.ratio * 100);
  253. if (ratio1 > -1) { // avoid NaN
  254.  
  255. // countdownDurationMs
  256. // 600000 - 0.2% <1% = 6s> <0.2% = 1.2s>
  257. // 300000 - 0.5% <1% = 3s> <0.5% = 1.5s>
  258. // 150000 - 1% <1% = 1.5s>
  259. // 75000 - 2% <1% =0.75s > <2% = 1.5s>
  260. // 30000 - 5% <1% =0.3s > <5% = 1.5s>
  261.  
  262. // 99px * 5% = 4.95px
  263.  
  264. // 15000 - 10% <1% =0.15s > <10% = 1.5s>
  265.  
  266.  
  267.  
  268.  
  269. // 1% Duration
  270.  
  271. let ratio2 = ratio1;
  272.  
  273. const ydd = yd.data;
  274. const d1 = ydd.durationSec;
  275. const d2 = ydd.fullDurationSec;
  276.  
  277. if (d1 === d2 && d1 > 1) {
  278.  
  279. if (d1 > 400) ratio2 = Math.round(ratio2 * 5) / 5; // 0.2%
  280. else if (d1 > 200) ratio2 = Math.round(ratio2 * 2) / 2; // 0.5%
  281. else if (d1 > 100) ratio2 = Math.round(ratio2 * 1) / 1; // 1%
  282. else if (d1 > 50) ratio2 = Math.round(ratio2 * 0.5) / 0.5; // 2%
  283. else if (d1 > 25) ratio2 = Math.round(ratio2 * 0.2) / 0.2; // 5% (max => 99px * 5% = 4.95px)
  284. else ratio2 = Math.round(ratio2 * 0.2) / 0.2;
  285.  
  286. } else {
  287. ratio2 = Math.round(ratio2 * 5) / 5; // 0.2% (min)
  288. }
  289.  
  290. // ratio2 = Math.round(ratio2 * 5) / 5;
  291. ratio2 = ratio2.toFixed(1)
  292. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  293.  
  294. if (yd.__style_last__ === v) return;
  295. yd.__style_last__ = v;
  296. // do not consider any delay here.
  297. // it shall be inside the looping for all properties changes. all the css background ops are in the same microtask.
  298.  
  299. }
  300.  
  301. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  302.  
  303.  
  304. } else {
  305. HTMLElement.prototype.setAttribute.apply(this, arguments);
  306. }
  307.  
  308. };
  309.  
  310. const fxOperator = (proto, propertyName) => {
  311. let propertyDescriptorGetter = null;
  312. try {
  313. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  314. } catch (e) { }
  315. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  316. };
  317.  
  318. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  319. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  320. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  321. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  322. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  323.  
  324.  
  325. /* globals WeakRef:false */
  326.  
  327. /** @type {(o: Object | null) => WeakRef | null} */
  328. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  329.  
  330. /** @type {(wr: Object | null) => Object | null} */
  331. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  332.  
  333. const watchUserCSS = () => {
  334.  
  335. // if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  336.  
  337. const getElemFromWR = (nr) => {
  338. const n = kRef(nr);
  339. if (n && n.isConnected) return n;
  340. return null;
  341. }
  342.  
  343. const clearContentVisibilitySizing = () => {
  344. Promise.resolve().then(() => {
  345.  
  346. let btnShowMoreWR = mWeakRef(document.querySelector('#show-more[disabled]'));
  347.  
  348. let lastVisibleItemWR = null;
  349. for (const elm of document.querySelectorAll('[wSr93]')) {
  350. if (elm.getAttribute('wSr93') === 'visible') lastVisibleItemWR = mWeakRef(elm);
  351. elm.setAttribute('wSr93', '');
  352. // custom CSS property --wsr94 not working when attribute wSr93 removed
  353. }
  354. requestAnimationFrame(() => {
  355. const btnShowMore = getElemFromWR(btnShowMoreWR); btnShowMoreWR = null;
  356. if (btnShowMore) btnShowMore.click();
  357. else {
  358. // would not work if switch it frequently
  359. const lastVisibleItem = getElemFromWR(lastVisibleItemWR); lastVisibleItemWR = null;
  360. if (lastVisibleItem) {
  361.  
  362. Promise.resolve()
  363. .then(() => lastVisibleItem.scrollIntoView())
  364. .then(() => lastVisibleItem.scrollIntoView(false))
  365. .then(() => lastVisibleItem.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }))
  366. .catch(e => { }) // break the chain when method not callable
  367.  
  368. }
  369. }
  370. })
  371.  
  372. })
  373.  
  374. }
  375.  
  376. const mutObserver = new MutationObserver((mutations) => {
  377. for (const mutation of mutations) {
  378. if ((mutation.addedNodes || 0).length >= 1) {
  379. for (const addedNode of mutation.addedNodes) {
  380. if (addedNode.nodeName === 'STYLE') {
  381. clearContentVisibilitySizing();
  382. return;
  383. }
  384. }
  385. }
  386. if ((mutation.removedNodes || 0).length >= 1) {
  387. for (const removedNode of mutation.removedNodes) {
  388. if (removedNode.nodeName === 'STYLE') {
  389. clearContentVisibilitySizing();
  390. return;
  391. }
  392. }
  393. }
  394. }
  395. });
  396.  
  397. mutObserver.observe(document.documentElement, {
  398. childList: true,
  399. subtree: false
  400. })
  401.  
  402. mutObserver.observe(document.head, {
  403. childList: true,
  404. subtree: false
  405. })
  406. mutObserver.observe(document.body, {
  407. childList: true,
  408. subtree: false
  409. });
  410.  
  411. }
  412.  
  413. const setupStyle = (m1, m2) => {
  414.  
  415. const dummy1v = {
  416. transform: '',
  417. height: '',
  418. minHeight: '',
  419. paddingBottom: '',
  420. paddingTop: ''
  421. };
  422. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  423. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  424. }
  425.  
  426. const dummy1p = proxyHelperFn(dummy1v);
  427. const sp1v = new Proxy(m1.style, dummy1p);
  428. const sp2v = new Proxy(m2.style, dummy1p);
  429. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  430. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  431. m1.removeAttribute("style");
  432. m2.removeAttribute("style");
  433.  
  434. }
  435.  
  436.  
  437. class WillChangeController {
  438. constructor(itemScroller, willChangeValue) {
  439. this.element = itemScroller;
  440. this.counter = 0;
  441. this.active = false;
  442. this.willChangeValue = willChangeValue;
  443. }
  444.  
  445. beforeOper() {
  446. if (!this.active) {
  447. this.active = true;
  448. this.element.style.willChange = this.willChangeValue;
  449. }
  450. this.counter++;
  451. }
  452.  
  453. afterOper() {
  454. const c = this.counter;
  455. requestAnimationFrame(() => {
  456. if (c === this.counter) {
  457. this.active = false;
  458. this.element.style.willChange = '';
  459. }
  460. })
  461. }
  462.  
  463. release() {
  464. const element = this.element;
  465. this.element = null;
  466. this.counter = 1e16;
  467. this.active = false;
  468. try {
  469. element.style.willChange = '';
  470. } catch (e) { }
  471. }
  472.  
  473. }
  474.  
  475.  
  476. customYtElements.onRegistryReady(() => {
  477.  
  478. let scrollWillChangeController = null;
  479. let contensWillChangeController = null;
  480.  
  481. // as it links to event handling, it has to be injected using immediateCallback
  482. customYtElements.whenRegistered('yt-live-chat-item-list-renderer', (cProto) => {
  483.  
  484. const mclp = cProto;
  485. console.assert(typeof mclp.scrollToBottom_ === 'function')
  486. console.assert(typeof mclp.scrollToBottom66_ !== 'function')
  487. console.assert(typeof mclp.flushActiveItems_ === 'function')
  488. console.assert(typeof mclp.flushActiveItems66_ !== 'function')
  489.  
  490.  
  491. mclp.__intermediate_delay__ = null;
  492.  
  493. mclp.scrollToBottom66_ = mclp.scrollToBottom_;
  494. mclp.scrollToBottom_ = function () {
  495. const cnt = this;
  496. const itemScroller = cnt.itemScroller;
  497. if (scrollWillChangeController && scrollWillChangeController.element !== itemScroller) {
  498. scrollWillChangeController.release();
  499. scrollWillChangeController = null;
  500. }
  501. if (!scrollWillChangeController) scrollWillChangeController = new WillChangeController(itemScroller, 'scroll-position');
  502. const wcController = scrollWillChangeController;
  503. wcController.beforeOper();
  504. cnt.__intermediate_delay__ = new Promise(resolve => {
  505. Promise.resolve().then(() => {
  506. cnt.scrollToBottom66_();
  507. }).then(() => {
  508. wcController.afterOper();
  509. resolve();
  510. });
  511. });
  512. }
  513.  
  514. mclp.flushActiveItems77_ = async function () {
  515. try {
  516.  
  517. const cnt = this;
  518. if (lastFlushActiveItemsCalled > 1e9) lastFlushActiveItemsCalled = 9;
  519. let tid = ++lastFlushActiveItemsCalled;
  520. if (tid !== lastFlushActiveItemsCalled || cnt.isAttached === false || (cnt.hostElement || cnt).isConnected === false) return;
  521. if (!cnt.activeItems_ || cnt.activeItems_.length === 0) return;
  522. if (cnt.canScrollToBottom_()) {
  523. let immd = cnt.__intermediate_delay__;
  524. await new Promise(requestAnimationFrame);
  525. if (tid !== lastFlushActiveItemsCalled || cnt.isAttached === false || (cnt.hostElement || cnt).isConnected === false) return;
  526. if (!cnt.activeItems_ || cnt.activeItems_.length === 0) return;
  527.  
  528. const items = (cnt.$ || 0).items;
  529. if (contensWillChangeController && contensWillChangeController.element !== items) {
  530. contensWillChangeController.release();
  531. contensWillChangeController = null;
  532. }
  533. if (!contensWillChangeController) contensWillChangeController = new WillChangeController(items, 'contents');
  534. const wcController = contensWillChangeController;
  535. cnt.__intermediate_delay__ = Promise.all([cnt.__intermediate_delay__ || null, immd || null]);
  536. wcController.beforeOper();
  537. await Promise.resolve();
  538. const len1 = cnt.activeItems_.length;
  539. cnt.flushActiveItems66_();
  540. const len2 = cnt.activeItems_.length;
  541. let bAsync = len1 !== len2;
  542. await Promise.resolve();
  543. if (bAsync) {
  544. cnt.async(() => {
  545. wcController.afterOper();
  546. });
  547. } else {
  548. wcController.afterOper();
  549. }
  550. return 1;
  551. } else {
  552. cnt.flushActiveItems66_();
  553. return 2;
  554. }
  555. } catch (e) {
  556. console.warn(e);
  557. }
  558. }
  559.  
  560. mclp.flushActiveItems66_ = mclp.flushActiveItems_;
  561. let lastFlushActiveItemsCalled = 0;
  562. mclp.flushActiveItems_ = function () {
  563. const cnt = this;
  564.  
  565. if (arguments.length !== 0 || !cnt.activeItems_ || !cnt.canScrollToBottom_) return cnt.flushActiveItems66_.apply(this, arguments);
  566.  
  567. if (cnt.activeItems_.length === 0) {
  568. cnt.__intermediate_delay__ = null;
  569. return;
  570. }
  571.  
  572. const cntData = ((cnt || 0).data || 0);
  573. if (cntData.maxItemsToDisplay > 90) cntData.maxItemsToDisplay = 90;
  574.  
  575. // ignore previous __intermediate_delay__ and create a new one
  576. cnt.__intermediate_delay__ = new Promise(resolve => {
  577. cnt.flushActiveItems77_().then(rt => {
  578. if (rt === 1) resolve(1); // success, scroll to bottom
  579. else if (rt === 2) resolve(2); // success, trim
  580. else resolve(-1); // skip
  581. });
  582. });
  583.  
  584. }
  585.  
  586. mclp.async66 = mclp.async;
  587. mclp.async = function () {
  588. // ensure the previous operation is done
  589. // .async is usually after the time consuming functions like flushActiveItems_ and scrollToBottom_
  590.  
  591. const stack = new Error().stack;
  592. const isFlushAsync = stack.indexOf('flushActiveItems_') >= 0;
  593. (this.__intermediate_delay__ || Promise.resolve()).then(rk => {
  594. if (isFlushAsync) {
  595. if (rk < 0) return;
  596. if (rk === 2 && arguments[0] === this.maybeScrollToBottom_) return;
  597. }
  598. this.async66.apply(this, arguments);
  599. });
  600.  
  601. }
  602.  
  603. })
  604.  
  605. });
  606.  
  607. const getProto = (element) => {
  608. let proto = null;
  609. if (element) {
  610. if (element.inst) proto = element.inst.constructor.prototype;
  611. else proto = element.constructor.prototype;
  612. }
  613. return proto || null;
  614. }
  615.  
  616. let done = 0;
  617. let main = async (q) => {
  618.  
  619. if (done) return;
  620.  
  621. if (!q) return;
  622. let m1 = nodeParent(q);
  623. let m2 = q;
  624. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  625.  
  626. done = 1;
  627.  
  628. Promise.resolve().then(watchUserCSS);
  629.  
  630. addCss();
  631.  
  632. setupStyle(m1, m2);
  633.  
  634. let lcRendererWR = null;
  635.  
  636. const lcRendererElm = () => {
  637. let lcRenderer = kRef(lcRendererWR);
  638. if (!lcRenderer || !lcRenderer.isConnected) {
  639. lcRenderer = document.querySelector('yt-live-chat-item-list-renderer.yt-live-chat-renderer');
  640. lcRendererWR = lcRenderer ? mWeakRef(lcRenderer) : null;
  641. }
  642. return lcRenderer
  643. };
  644.  
  645. let hasFirstShowMore = false;
  646.  
  647. const visObserverFn = (entry) => {
  648.  
  649. const target = entry.target;
  650. if (!target) return;
  651. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  652. const h = entry.boundingClientRect.height;
  653. if (h < 16) { // wrong: 8 (padding/margin); standard: 32; test: 16 or 20
  654. // e.g. under fullscreen. the element created but not rendered.
  655. target.setAttribute('wSr93', '');
  656. return;
  657. }
  658. if (isVisible) {
  659. target.style.setProperty('--wsr94', h + 'px');
  660. target.setAttribute('wSr93', 'visible');
  661. if (nNextElem(target) === null) {
  662. // firstVisibleItemDetected = true;
  663. /*
  664. if (dateNow() - lastScroll < 80) {
  665. lastLShow = 0;
  666. lastScroll = 0;
  667. Promise.resolve().then(clickShowMore);
  668. } else {
  669. lastLShow = dateNow();
  670. }
  671. */
  672. // lastLShow = dateNow();
  673. } else if (!hasFirstShowMore) { // should more than one item being visible
  674. // implement inside visObserver to ensure there is sufficient delay
  675. hasFirstShowMore = true;
  676. requestAnimationFrame(() => {
  677. // foreground page
  678. // activeDeferredAppendChild = true;
  679. // page visibly ready -> load the latest comments at initial loading
  680. const lcRenderer = lcRendererElm();
  681. if (lcRenderer) {
  682. (lcRenderer.inst || lcRenderer).scrollToBottom_();
  683. }
  684. });
  685. }
  686. }
  687. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  688.  
  689. target.style.setProperty('--wsr94', h + 'px');
  690. target.setAttribute('wSr93', 'hidden');
  691. } // note: might consider 0 < entry.intersectionRatio < 0.5 and target.getAttribute('wSr93') === '' <new last item>
  692.  
  693. }
  694.  
  695. const visObserver = new IntersectionObserver((entries) => {
  696.  
  697. for (const entry of entries) {
  698.  
  699. Promise.resolve(entry).then(visObserverFn);
  700.  
  701. }
  702.  
  703. }, {
  704. /*
  705. root: items,
  706. rootMargin: "0px",
  707. threshold: 1.0,
  708. */
  709. // root: HTMLElement.prototype.closest.call(m2, '#item-scroller.yt-live-chat-item-list-renderer'), // nullable
  710. rootMargin: "0px",
  711. threshold: [0.05, 0.95],
  712. });
  713.  
  714. //m2.style.visibility='';
  715.  
  716. const mutFn = (items) => {
  717. for (let node = nLastElem(items); node !== null; node = nPrevElem(node)) {
  718. if (node.hasAttribute('wSr93')) break;
  719. node.setAttribute('wSr93', '');
  720. visObserver.observe(node);
  721. }
  722. }
  723.  
  724. const mutObserver = new MutationObserver((mutations) => {
  725. const items = (mutations[0] || 0).target;
  726. if (!items) return;
  727. mutFn(items);
  728. });
  729.  
  730. const setupMutObserver = (m2) => {
  731. mutObserver.disconnect();
  732. mutObserver.takeRecords();
  733. if (m2) {
  734. mutObserver.observe(m2, {
  735. childList: true,
  736. subtree: false
  737. });
  738. mutFn(m2);
  739. }
  740. }
  741.  
  742. setupMutObserver(m2);
  743.  
  744. const mclp = getProto(document.querySelector('yt-live-chat-item-list-renderer'));
  745. if (mclp && mclp.attached) {
  746.  
  747. mclp.attached66 = mclp.attached;
  748. mclp.attached = function () {
  749. let m2 = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');
  750. let m1 = nodeParent(m2);
  751. setupStyle(m1, m2);
  752. setupMutObserver(m2);
  753. return this.attached66();
  754. }
  755.  
  756. mclp.detached66 = mclp.detached;
  757. mclp.detached = function () {
  758. setupMutObserver();
  759. return this.detached66();
  760. }
  761.  
  762. mclp.canScrollToBottom_ = function () {
  763. return this.atBottom && this.allowScroll && !(dateNow() - lastWheel < 80)
  764. }
  765.  
  766. mclp.isSmoothScrollEnabled_ = function () {
  767. return false;
  768. }
  769.  
  770. } else {
  771. console.warn(`proto.attached for yt-live-chat-item-list-renderer is unavailable.`)
  772. }
  773.  
  774.  
  775. let scrollCount = 0;
  776. document.addEventListener('scroll', (evt) => {
  777. if (!evt || !evt.isTrusted) return;
  778. // lastScroll = dateNow();
  779. if (++scrollCount > 1e9) scrollCount = 9;
  780. }, { passive: true, capture: true }) // support contain => support passive
  781.  
  782. // document.addEventListener('scroll', (evt) => {
  783.  
  784. // if (!evt || !evt.isTrusted) return;
  785. // if (!firstVisibleItemDetected) return;
  786. // const isUserAction = dateNow() - lastWheel < 80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  787. // if (!isUserAction) return;
  788. // // lastScroll = dateNow();
  789.  
  790. // }, { passive: true, capture: true }) // support contain => support passive
  791.  
  792.  
  793. let lastScrollCount = -1;
  794. document.addEventListener('wheel', (evt) => {
  795.  
  796. if (!evt || !evt.isTrusted) return;
  797. if (lastScrollCount === scrollCount) return;
  798. lastScrollCount = scrollCount;
  799. lastWheel = dateNow();
  800.  
  801. }, { passive: true, capture: true }) // support contain => support passive
  802.  
  803.  
  804. const fp = (renderer) => {
  805. const cnt = renderer.inst || renderer;
  806. const container = (cnt.$ || 0).container;
  807. if (container) {
  808. container.setAttribute = tickerContainerSetAttribute;
  809. }
  810. }
  811. const tags = ["yt-live-chat-ticker-paid-message-item-renderer", "yt-live-chat-ticker-paid-sticker-item-renderer",
  812. "yt-live-chat-ticker-renderer", "yt-live-chat-ticker-sponsor-item-renderer"];
  813. for (const tag of tags) {
  814. const dummy = document.createElement(tag);
  815.  
  816. const cProto = getProto(dummy);
  817. if (!cProto || !cProto.attached) {
  818. console.warn(`proto.attached for ${tag} is unavailable.`)
  819. continue;
  820. }
  821.  
  822. const __updateTimeout__ = cProto.updateTimeout;
  823.  
  824. const canDoUpdateTimeoutReplacement = (() => {
  825.  
  826. if (dummy.countdownMs < 1 && dummy.lastCountdownTimeMs < 1 && dummy.countdownMs < 1 && dummy.countdownDurationMs < 1) {
  827. return typeof dummy.setContainerWidth === 'function' && typeof dummy.slideDown === 'function';
  828. }
  829. return false;
  830.  
  831. })(dummy.inst || dummy) && ((__updateTimeout__ + "").indexOf("window.requestAnimationFrame(this.updateTimeout.bind(this))") > 0);
  832.  
  833.  
  834.  
  835. if (canDoUpdateTimeoutReplacement) {
  836.  
  837. const killTicker = (cnt) => {
  838. if ("auto" === cnt.hostElement.style.width) cnt.setContainerWidth();
  839. cnt.slideDown()
  840. };
  841.  
  842. cProto.__ratio__ = null;
  843. cProto._updateTimeout21_ = function (a) {
  844.  
  845. /*
  846. let pRatio = this.countdownMs / this.countdownDurationMs;
  847. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  848. let noMoreCountDown = this.countdownMs < 1e-6;
  849. let qRatio = this.countdownMs / this.countdownDurationMs;
  850. if(noMoreCountDown){
  851. this.countdownMs = 0;
  852. this.ratio = 0;
  853. } else if( pRatio - qRatio < 0.001 && qRatio < pRatio){
  854.  
  855. }else{
  856. this.ratio = qRatio;
  857. }
  858. */
  859.  
  860. this.countdownMs -= (a - (this.lastCountdownTimeMs || 0));
  861.  
  862. let currentRatio = this.__ratio__;
  863. let tdv = this.countdownMs / this.countdownDurationMs;
  864. let nextRatio = Math.round(tdv * 500) / 500; // might generate 0.143000000001
  865.  
  866. const validCountDown = nextRatio > 0;
  867. const isAttached = this.isAttached;
  868.  
  869. if (!validCountDown) {
  870.  
  871. this.lastCountdownTimeMs = null;
  872.  
  873. this.countdownMs = 0;
  874. this.__ratio__ = null;
  875. this.ratio = 0;
  876.  
  877. if (isAttached) Promise.resolve(this).then(killTicker);
  878.  
  879. } else if (!isAttached) {
  880.  
  881. this.lastCountdownTimeMs = null;
  882.  
  883. } else {
  884.  
  885. this.lastCountdownTimeMs = a;
  886.  
  887. const ratioDiff = currentRatio - nextRatio; // 0.144 - 0.142 = 0.002
  888. if (ratioDiff < 0.001 && ratioDiff > -1e-6) {
  889. // ratioDiff = 0
  890.  
  891. } else {
  892. // ratioDiff = 0.002 / 0.004 ....
  893. // OR ratioDiff < 0
  894.  
  895. this.__ratio__ = nextRatio;
  896.  
  897. this.ratio = nextRatio;
  898. }
  899.  
  900. return true;
  901. }
  902.  
  903. };
  904.  
  905. cProto._updateTimeout21_ = function (a) {
  906. this.countdownMs = Math.max(0, this.countdownMs - (a - (this.lastCountdownTimeMs || 0)));
  907. this.ratio = this.countdownMs / this.countdownDurationMs;
  908. if (this.isAttached && this.countdownMs) {
  909. this.lastCountdownTimeMs = a;
  910. return true;
  911. } else {
  912. this.lastCountdownTimeMs = null;
  913. if (this.isAttached) {
  914. ("auto" === this.hostElement.style.width && this.setContainerWidth(), this.slideDown())
  915. }
  916. }
  917. }
  918.  
  919.  
  920. // temporarily removed; buggy for playback
  921. /*
  922. cProto.updateTimeout = async function (a) {
  923.  
  924. let ret = this._updateTimeout21_(a);
  925. while (ret) {
  926. let a = await new Promise(resolve => {
  927. this.rafId = requestAnimationFrame(resolve)
  928. }); // could be never resolve
  929. ret = this._updateTimeout21_(a);
  930. }
  931.  
  932. };
  933. */
  934.  
  935.  
  936. }
  937.  
  938. cProto.attached77 = cProto.attached
  939.  
  940. cProto.attached = function () {
  941. fp(this.hostElement || this);
  942. return this.attached77();
  943. }
  944.  
  945. for (const elm of document.getElementsByTagName(tag)) {
  946. fp(elm);
  947. }
  948.  
  949.  
  950. }
  951.  
  952. };
  953.  
  954.  
  955. function onReady() {
  956. let tmObserver = new MutationObserver(() => {
  957.  
  958. let p = document.getElementById('items'); // fast
  959. if (!p) return;
  960. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  961.  
  962. if (q) {
  963. tmObserver.disconnect();
  964. tmObserver.takeRecords();
  965. tmObserver = null;
  966. Promise.resolve(q).then((q) => {
  967. // confirm Promis.resolve() is resolveable
  968. // execute main without direct blocking
  969. main(q);
  970. })
  971. }
  972.  
  973. });
  974.  
  975. tmObserver.observe(document.body || document.documentElement, {
  976. childList: true,
  977. subtree: true
  978. });
  979.  
  980. }
  981.  
  982. Promise.resolve().then(() => {
  983.  
  984. if (document.readyState !== 'loading') {
  985. onReady();
  986. } else {
  987. window.addEventListener("DOMContentLoaded", onReady, false);
  988. }
  989.  
  990. });
  991.  
  992. })({ Promise, requestAnimationFrame, IntersectionObserver });