YouTube 超快聊天

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

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

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @name:ja YouTube スーパーファーストチャット
  4. // @name:zh-TW YouTube 超快聊天
  5. // @name:zh-CN YouTube 超快聊天
  6. // @namespace UserScript
  7. // @match https://www.youtube.com/live_chat*
  8. // @version 0.1.0
  9. // @license MIT
  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. const addCss = () => document.head.appendChild(document.createElement('style')).textContent = `
  26.  
  27. @supports (contain: layout paint style) and (content-visibility: auto) and (contain-intrinsic-size: auto var(--wsr94)) {
  28.  
  29. [wSr93] {
  30. content-visibility: visible;
  31. }
  32.  
  33. [wSr93="hidden"]:nth-last-child(n+4) {
  34. content-visibility: auto;
  35. contain-intrinsic-size: auto var(--wsr94);
  36. }
  37.  
  38. }
  39.  
  40. @supports (contain: layout paint style) {
  41.  
  42.  
  43. /* optional */
  44. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  45. height: auto !important;
  46. min-height: unset !important;
  47. }
  48.  
  49. #items.style-scope.yt-live-chat-item-list-renderer {
  50. transform: translateY(0px) !important;
  51. /*padding-bottom: 0 !important;
  52. padding-top: 0 !important;*/
  53. }
  54.  
  55. /* optional */
  56.  
  57. yt-icon[icon="down_arrow"] > *,
  58. yt-icon-button#show-more > * {
  59. pointer-events: none !important;
  60. }
  61.  
  62.  
  63. #item-list.style-scope.yt-live-chat-renderer,
  64. yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer,
  65. #item-list.style-scope.yt-live-chat-renderer *,
  66. yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer * {
  67. will-change: unset !important;
  68. }
  69.  
  70. yt-img-shadow[height][width] {
  71. content-visibility: visible !important;
  72. }
  73.  
  74.  
  75. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
  76. position: static !important;
  77. }
  78.  
  79.  
  80. /* ------------------------------------------------------------------------------------------------------------- */
  81.  
  82. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
  83. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
  84. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
  85. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  86. contain: layout style;
  87. }
  88.  
  89. /*
  90. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
  91. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
  92. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
  93. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  94. contain: layout style;
  95. display: inline-flex;
  96. vertical-align: middle;
  97. }
  98. */
  99.  
  100. #items yt-live-chat-text-message-renderer {
  101. contain: layout style;
  102. }
  103.  
  104. yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
  105. overflow-y: scroll;
  106. padding-right: 0;
  107. }
  108.  
  109. body yt-live-chat-app {
  110. contain: size layout paint style;
  111. overflow: hidden;
  112. }
  113.  
  114. #items.style-scope.yt-live-chat-item-list-renderer {
  115. contain: layout paint style;
  116. }
  117.  
  118. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  119. contain: style;
  120. }
  121.  
  122. #item-scroller.style-scope.yt-live-chat-item-list-renderer {
  123. contain: size style;
  124. }
  125.  
  126. #contents.style-scope.yt-live-chat-item-list-renderer,
  127. #chat.style-scope.yt-live-chat-renderer,
  128. img.style-scope.yt-img-shadow[width][height] {
  129. contain: size layout paint style;
  130. }
  131.  
  132. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label],
  133. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
  134. contain: layout paint style;
  135. }
  136.  
  137. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer,
  138. yt-live-chat-membership-item-renderer.style-scope.yt-live-chat-item-list-renderer,
  139. yt-live-chat-paid-message-renderer.style-scope.yt-live-chat-item-list-renderer,
  140. 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.  
  163. #continuations, #continuations * {
  164. contain: strict;
  165. position: fixed;
  166. top: 2px;
  167. height: 1px;
  168. width: 2px;
  169. height: 1px;
  170. visibility: collapse;
  171. }
  172.  
  173.  
  174. }
  175.  
  176. `;
  177.  
  178. const { Promise, requestAnimationFrame } = __CONTEXT__;
  179.  
  180. let activator = false;
  181.  
  182. let mpws = new WeakSet();
  183. let ops = [];
  184. let msqs = new Set();
  185.  
  186. const sp7 = Symbol();
  187.  
  188.  
  189.  
  190.  
  191. const phFn = (dummy) => ({
  192.  
  193. get(target, prop) {
  194. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  195. },
  196. set(target, prop, value) {
  197. if (!(prop in dummy)) {
  198. target[prop] = value;
  199. }
  200. return true;
  201.  
  202. },
  203. has(target, prop) {
  204. return (prop in target)
  205. },
  206. deleteProperty(target, prop) {
  207.  
  208. return true;
  209. },
  210. ownKeys(target) {
  211. return Object.keys(target);
  212. },
  213. defineProperty(target, key, descriptor) {
  214. return Object.defineProperty(target, key, descriptor);
  215. // return true;
  216. },
  217. getOwnPropertyDescriptor(target, key) {
  218. return Object.getOwnPropertyDescriptor(target, key);
  219. },
  220.  
  221.  
  222.  
  223. });
  224.  
  225.  
  226. const dummy3v = {
  227. "background": "",
  228. "backgroundAttachment": "",
  229. "backgroundBlendMode": "",
  230. "backgroundClip": "",
  231. "backgroundColor": "",
  232. "backgroundImage": "",
  233. "backgroundOrigin": "",
  234. "backgroundPosition": "",
  235. "backgroundPositionX": "",
  236. "backgroundPositionY": "",
  237. "backgroundRepeat": "",
  238. "backgroundRepeatX": "",
  239. "backgroundRepeatY": "",
  240. "backgroundSize": ""
  241. };
  242. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  243. dummy3v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  244. }
  245.  
  246. // const dummy3p = phFn(dummy3v);
  247.  
  248. const pt2DecimalFixer = (x) => Math.round(x * 5, 0) / 5;
  249.  
  250. const tickerContainerSetAttribute = function (attrName, attrValue) {
  251.  
  252. let yd = (this.__dataHost || 0).__data;
  253.  
  254. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  255.  
  256. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  257. let v = `${attrValue}`;
  258. // conside a ticker is 101px width
  259. // 1% = 1.01px
  260. // 0.2% = 0.202px
  261.  
  262. const ratio1 = (yd.ratio * 100);
  263. const ratio2 = pt2DecimalFixer(ratio1);
  264. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  265.  
  266. if (yd.__style_last__ === v) return;
  267. yd.__style_last__ = v;
  268.  
  269. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  270.  
  271.  
  272.  
  273. } else {
  274. HTMLElement.prototype.setAttribute.apply(this, arguments);
  275. }
  276.  
  277. };
  278.  
  279.  
  280. /*
  281. *
  282. * const tickerContainerSetAttribute = function (attrName, attrValue) {
  283.  
  284. const yd = (this.__dataHost||0).__data;
  285. if (arguments.length === 2 && attrName === 'style' && attrValue && yd){
  286. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  287. let v = attrValue;
  288.  
  289. // conside a ticker is 101px width
  290. // 1% = 1.01px
  291. // 0.2% = 0.202px
  292. const ratio1 = (yd.ratio * 100);
  293. const ratio2 = pt2DecimalFixer(ratio1);
  294. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  295.  
  296. console.log(ratio1, ratio2)
  297. if (yd.__style_last__ !== v) {
  298. yd.__style_last__ = v; // clear along with data change
  299.  
  300. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  301. return;
  302. }
  303.  
  304.  
  305. }
  306. return HTMLElement.prototype.setAttribute.apply(this, arguments);
  307.  
  308. };
  309.  
  310. */
  311.  
  312.  
  313.  
  314. Node.prototype.appendChild = ((appendChild) => (function (s) {
  315. if (arguments.length !== 1) return appendChild.apply(this, arguments);
  316. // console.log(34, 1, this.is, this.nodeName, activator, s.nodeName)
  317. const stack = new Error().stack;
  318.  
  319. if (activator && (msqs.has(stack) || s.nodeName === 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER') && typeof s.is === 'string') {
  320. msqs.add(stack);
  321. // this = '#document-fragment'
  322. mpws.add(this);
  323. if (ops.length === 0) requestAnimationFrame(() => {
  324. const e = [...ops]
  325. ops.length = 0;
  326. for (const t of e) t();
  327. });
  328. ops.push(() => {
  329. mpws.delete(this)
  330. appendChild.apply(this, arguments);
  331. })
  332. return s;
  333. } else if (activator && mpws.has(s)) {
  334.  
  335. ops.push(() => {
  336. appendChild.apply(this, arguments);
  337. })
  338. return s;
  339. } else if (this.nodeName === 'YT-LIVE-CHAT-TICKER-PAID-MESSAGE-ITEM-RENDERER') {
  340.  
  341.  
  342.  
  343. appendChild.call(this, s);
  344.  
  345. let container = this.$.container;
  346. if (container) {
  347.  
  348. // const sp3v = new Proxy(container.style, dummy3p)
  349.  
  350. // Object.defineProperty(container, 'style', {get(){return sp3v}, set() { }, enumerable: true, configurable: true });
  351.  
  352.  
  353. container.setAttribute = tickerContainerSetAttribute;
  354.  
  355.  
  356. }
  357.  
  358. return s;
  359. }
  360. // if(activator) return null;
  361. appendChild.call(this, s);
  362. return s;
  363. }))(Node.prototype.appendChild);
  364.  
  365. /*
  366. Node.prototype.append = ((append) => (function () {
  367. // console.log(34,2 )
  368. return append.apply(this, arguments);
  369. }))(Node.prototype.append);
  370.  
  371. Node.prototype.insertBefore = ((insertBefore) => (function () {
  372. // console.log(34,3, this.is, this.nodeName, activator)
  373. // if(activator) return null;
  374. return insertBefore.apply(this, arguments);
  375. }))(Node.prototype.insertBefore);
  376.  
  377. Node.prototype.insertAfter = ((insertAfter) => (function () {
  378. // console.log(34,4)
  379. return insertAfter.apply(this, arguments);
  380. }))(Node.prototype.insertAfter);
  381.  
  382. */
  383.  
  384.  
  385. const isContainSupport = CSS.supports('contain', 'layout paint style');
  386. if (!isContainSupport) {
  387. console.error(`
  388. YouTube Light Chat Scroll: Your browser does not support 'contain'.
  389. Chrome >= 52; Edge >= 79; Safari >= 15.4, Firefox >= 69; Opera >= 39
  390. `.trim());
  391. return;
  392. }
  393.  
  394.  
  395. const fxOperator = (proto, propertyName) => {
  396. let propertyDescriptorGetter = null;
  397. try {
  398. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  399. } catch (e) { }
  400. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  401. };
  402.  
  403. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  404. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  405. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  406. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  407. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  408.  
  409.  
  410. /* globals WeakRef:false */
  411.  
  412. /** @type {(o: Object | null) => WeakRef | null} */
  413. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  414.  
  415. /** @type {(wr: Object | null) => Object | null} */
  416. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  417.  
  418. let done = 0;
  419. let main = async (q) => {
  420.  
  421. if (done) return;
  422.  
  423. if (!q) return;
  424. let m1 = nodeParent(q);
  425. let m2 = q;
  426. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  427.  
  428. done = 1;
  429.  
  430. addCss();
  431.  
  432. const dummy1v = {
  433. transform: '',
  434. height: '',
  435. maxHeight: '',
  436. paddingBottom: '',
  437. paddingTop: ''
  438. };
  439. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  440. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  441. }
  442.  
  443.  
  444.  
  445. const dummy1p = phFn(dummy1v);
  446. const sp1v = new Proxy(m1.style, dummy1p);
  447. const sp2v = new Proxy(m2.style, dummy1p);
  448. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  449. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  450. m1.removeAttribute("style");
  451. m2.removeAttribute("style");
  452.  
  453. let lastClick = 0;
  454. document.addEventListener('click', (evt) => {
  455. if (!evt.isTrusted) return;
  456. const target = ((evt || 0).target || 0)
  457. if (target.id === 'show-more') {
  458. if (target.nodeName !== 'YT-ICON-BUTTON') return;
  459.  
  460. if (Date.now() - lastClick < 80) return;
  461. requestAnimationFrame(() => {
  462. lastClick = Date.now();
  463. target.click();
  464. })
  465. }
  466.  
  467. })
  468.  
  469. let btnShowMoreWR = null;
  470.  
  471. const clickShowMore = () => {
  472. let btnShowMore = kRef(btnShowMoreWR);
  473. if (!btnShowMore || !btnShowMore.isConnected) {
  474. btnShowMore = document.querySelector('#show-more.yt-live-chat-item-list-renderer');
  475. btnShowMoreWR = mWeakRef(btnShowMore);
  476. }
  477. if (btnShowMore) btnShowMore.click();
  478. };
  479.  
  480. let hasFirstShowMore = false;
  481.  
  482. const visObserver = new IntersectionObserver((entries) => {
  483.  
  484. for (const entry of entries) {
  485.  
  486. const target = entry.target;
  487. if (!target) continue;
  488. if (entry.isIntersecting === true) {
  489. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  490. target.setAttribute('wSr93', 'visible');
  491. if (nNextElem(target) === null) {
  492. Promise.resolve().then(clickShowMore);
  493. } else if (!hasFirstShowMore) {
  494. // implement inside visObserver to ensure there is sufficient delay
  495. hasFirstShowMore = true;
  496. requestAnimationFrame(() => {
  497. // page visibly ready -> load the fresh comments
  498. clickShowMore();
  499. activator = true;
  500. });
  501. }
  502. }
  503. else if (target.getAttribute('wSr93') === 'visible') {
  504. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  505. target.setAttribute('wSr93', 'hidden');
  506. }
  507.  
  508. }
  509.  
  510. }, {
  511. /*
  512. root: items,
  513. rootMargin: "0px",
  514. threshold: 1.0,
  515. */
  516. });
  517.  
  518. const mutObserver = new MutationObserver((mutations) => {
  519. const items = (mutations[0] || 0).target;
  520. if (!items) return;
  521. let node = nLastElem(items);
  522. for (; node !== null; node = nPrevElem(node)) {
  523. if (node.hasAttribute('wSr93')) break;
  524. node.setAttribute('wSr93', '');
  525. visObserver.observe(node);
  526. }
  527. });
  528. mutObserver.observe(m2, {
  529. childList: true,
  530. subtree: false
  531. });
  532.  
  533.  
  534. /** @type {HTMLElement} */
  535. let c1 = nPrevElem(m1);
  536. if (c1 && c1.id === "live-chat-banner") {
  537. let rsObserver = new ResizeObserver((entries) => {
  538.  
  539. for (const entry of entries) {
  540. const target = entry.target;
  541. if (target && target.id === "live-chat-banner") {
  542. let p = entry.borderBoxSize ? (entry.borderBoxSize[0] || 0).blockSize : 0;
  543. let c1h = p > entry.contentRect.height ? p : entry.contentRect.height + 16;
  544. document.documentElement.style.setProperty('--items-top-padding', (Math.ceil(c1h / 2) * 2) + 'px');
  545. }
  546. }
  547.  
  548. });
  549. rsObserver.observe(c1);
  550. }
  551.  
  552.  
  553.  
  554. };
  555.  
  556.  
  557.  
  558. function onReady() {
  559. let tmObserver = new MutationObserver(() => {
  560.  
  561.  
  562. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');
  563.  
  564. if (q) {
  565. tmObserver.disconnect();
  566. tmObserver.takeRecords();
  567. tmObserver = null;
  568. Promise.resolve(q).then((q) => {
  569. // confirm Promis.resolve() is resolveable
  570. // execute main without direct blocking
  571. main(q);
  572. })
  573. }
  574.  
  575. });
  576.  
  577. tmObserver.observe(document.body, {
  578. childList: true,
  579. subtree: true
  580. });
  581.  
  582. }
  583.  
  584.  
  585.  
  586. if (document.readyState != 'loading') {
  587. onReady();
  588. } else {
  589. window.addEventListener("DOMContentLoaded", onReady, false);
  590. }
  591.  
  592.  
  593. })({ Promise, requestAnimationFrame });