YouTube Hide Chat by Default

Hides chat on YouTube live streams by default

  1. // ==UserScript==
  2. // @name YouTube Hide Chat by Default
  3. // @namespace https://skoshy.com
  4. // @version 0.8.0
  5. // @description Hides chat on YouTube live streams by default
  6. // @author Stefan K.
  7. // @match https://www.youtube.com/*
  8. // @grant GM.getValue
  9. // @grant GM.setValue
  10. // @icon https://youtube.com/favicon.ico
  11. // ==/UserScript==
  12.  
  13. const scriptId = "youtube-hide-chat-by-default";
  14.  
  15. const CHANNELS_BLOCKLIST = [
  16. // you can place channel IDs here to block them from hiding their chat automatically
  17. // example: 'UCTSCjjnCuAPHcfQWNNvULTw'
  18. ];
  19.  
  20. const isInIframe = () => window.top !== window.self;
  21.  
  22. const UNIQUE_ID = (function getUniqueId() {
  23. if (isInIframe()) {
  24. const capturedUniqueId = new URL(window.location.href).searchParams.get(`${scriptId}-unique-id`);
  25.  
  26. if (!capturedUniqueId) {
  27. throw new Error(`Unique ID was not properly passed to iFrame: ${window.location.href}`);
  28. }
  29.  
  30. log('Running in an iFrame, grabbed unique ID from URL', capturedUniqueId, window.location.href);
  31.  
  32. return capturedUniqueId;
  33. }
  34.  
  35. return Math.floor(Math.random()*1000000);
  36. })();
  37.  
  38. function log(...toLog) {
  39. console.log(`[${scriptId}]:`, ...toLog);
  40. }
  41.  
  42. const StorageClass = (scriptId, uniqueId, allowedKeys) => {
  43. (async function updateSubStorageIds() {
  44. const subStorageKey = `${scriptId}_base_subStorageIds`;
  45. const subStorageIds = JSON.parse((await GM.getValue(subStorageKey)) || '{}');
  46. console.log({subStorageIds});
  47. await GM.setValue(subStorageKey, JSON.stringify({
  48. ...subStorageIds,
  49. [uniqueId]: {
  50. dateCreated: Date.now(),
  51. },
  52. }));
  53. const newSubStorageIds = (await GM.getValue(subStorageKey)) || {};
  54. console.log('Set the value for subStorageIds', newSubStorageIds);
  55. })();
  56.  
  57. const setVal = async (key, val) => {
  58. if (!allowedKeys.includes(key)) {
  59. throw new Error('Key not allowed');
  60. }
  61.  
  62. await GM.setValue(`${scriptId}_${uniqueId}_${key}`, val);
  63. }
  64.  
  65. const getVal = async (key) => {
  66. if (!allowedKeys.includes(key)) {
  67. throw new Error('Key not allowed');
  68. }
  69.  
  70. return GM.getValue(`${scriptId}_${uniqueId}_${key}`);
  71. };
  72.  
  73. return { setVal, getVal };
  74. };
  75.  
  76. const { setVal, getVal } = StorageClass(scriptId, UNIQUE_ID, ['lastVidThatHidChat']);
  77.  
  78. (function() {
  79. "use strict";
  80.  
  81. // - if youtube decides to use a new button type, add it here
  82. const buttonSelectors = ["button"];
  83. const mutationObserverSelectors = [...buttonSelectors, 'iframe'];
  84.  
  85. function getRootUrlSearchParams() {
  86. return new URL(window.location.href).searchParams;
  87. }
  88.  
  89. function getCurrentVideoId() {
  90. const v = getRootUrlSearchParams().get('v');
  91.  
  92. if (v) {
  93. log('Got Video ID from URL Search Params', v);
  94. return v;
  95. }
  96.  
  97. if (isInIframe()) {
  98. // if not the parent frame, then get it from the passed in iframe url params
  99. const passedThroughId = getRootUrlSearchParams().get(`${scriptId}-current-video-id`);
  100. log('Not parent frame, getting video ID from passed through ID', passedThroughId);
  101. return passedThroughId;
  102. }
  103.  
  104. return null;
  105. }
  106.  
  107. function getCurrentVideoChannelId() {
  108. const channelId = document.querySelector('a[aria-label="About"][href*="channel/"]')?.getAttribute('href')?.match(/\/channel\/(.+)[\/$]/)?.[1];
  109.  
  110. if (channelId) {
  111. return channelId;
  112. }
  113.  
  114. if (isInIframe()) {
  115. // if not the parent frame, then get it from the passed in iframe url params
  116. const passedThroughId = getRootUrlSearchParams().get(`${scriptId}-current-channel-id`);
  117. log('Not parent frame, getting channel ID from passed through ID', passedThroughId);
  118.  
  119. if (passedThroughId === 'null' || !passedThroughId) {
  120. log('ERROR: There\'s a problem parsing the Channel ID, blocklist functionality will not work', passedThroughId);
  121. return null;
  122. }
  123.  
  124. return passedThroughId;
  125. }
  126.  
  127. return null;
  128. }
  129.  
  130. function findAncestorOfElement(el, findFunc) {
  131. let currentEl = el;
  132.  
  133. while (currentEl?.parentElement) {
  134. const result = findFunc(currentEl.parentElement);
  135.  
  136. if (result) {
  137. return currentEl.parentElement;
  138. }
  139.  
  140. currentEl = currentEl.parentElement;
  141. }
  142.  
  143. return undefined;
  144. }
  145.  
  146. function isHideChatButton(node) {
  147. const youtubeLiveChatAppAncestor = findAncestorOfElement(node, (parentEl) => {
  148. return parentEl.tagName === 'YT-LIVE-CHAT-APP';
  149. });
  150.  
  151. if (!youtubeLiveChatAppAncestor) {
  152. return false;
  153. }
  154.  
  155. return (node.getAttribute('aria-label') === 'Close');
  156. }
  157.  
  158. function addedNodeHandler(node) {
  159. if (!node.matches) return;
  160.  
  161. if (node.matches('iframe')) {
  162. handleAddedIframe(node);
  163. return;
  164. }
  165.  
  166. if (
  167. !buttonSelectors.some(b => node.matches(b))
  168. ) {
  169. return;
  170. }
  171.  
  172. if (isHideChatButton(node)) {
  173. log(`Found a hide-chat button`, node);
  174.  
  175. const currentVid = getCurrentVideoId();
  176. const currentChannelId = getCurrentVideoChannelId();
  177. const lastVidThatHidChat = getVal('lastVidThatHidChat');
  178.  
  179. if (lastVidThatHidChat === currentVid) {
  180. log(`Already automatically triggered to hide chat for this video`, { lastVidThatHidChat, currentVid, currentChannelId });
  181. return;
  182. }
  183.  
  184. if (CHANNELS_BLOCKLIST.includes(currentChannelId)) {
  185. log(`Channel in blocklist`, { lastVidThatHidChat, currentVid, currentChannelId });
  186. return;
  187. }
  188.  
  189. log(`Attempting to hide the chat by default`, { lastVidThatHidChat, currentVid, currentChannelId });
  190.  
  191. setVal('lastVidThatHidChat', currentVid);
  192.  
  193. node.click();
  194. }
  195. }
  196.  
  197. function handleAddedIframe(node) {
  198. if (node.getAttribute(`${scriptId}-modified-src`)) {
  199. return;
  200. }
  201.  
  202. const url = new URL(node.src);
  203. url.searchParams.set(`${scriptId}-unique-id`, UNIQUE_ID);
  204. url.searchParams.set(`${scriptId}-current-video-id`, getCurrentVideoId());
  205. url.searchParams.set(`${scriptId}-current-channel-id`, getCurrentVideoChannelId());
  206. log('New iFrame URL', url.toString());
  207.  
  208. node.src = url.toString();
  209. node.setAttribute(`${scriptId}-modified-src`, true);
  210. }
  211.  
  212. /*
  213. const bodyObserver = new MutationObserver(function(mutations) {
  214. mutations.forEach(function(mutation) {
  215. const newNodes = [];
  216.  
  217. mutation.addedNodes.forEach(addedNode => {
  218. newNodes.push(addedNode);
  219.  
  220. // it might be text node or comment node which don't have querySelectorAll
  221. if (addedNode.querySelectorAll) {
  222. mutationObserverSelectors.forEach(bs => {
  223. addedNode.querySelectorAll(bs).forEach((n) => {
  224. newNodes.push(n);
  225. });
  226. });
  227. }
  228. });
  229.  
  230. newNodes.forEach(n => addedNodeHandler(n));
  231. });
  232. });
  233. */
  234.  
  235. setInterval(() =>
  236. Array.from(
  237. document.querySelectorAll(mutationObserverSelectors.join(', '))
  238. ).forEach(n => addedNodeHandler(n))
  239. , 3000);
  240.  
  241. /*
  242. bodyObserver.observe(document, {
  243. attributes: true,
  244. childList: true,
  245. subtree: true,
  246. characterData: true
  247. });
  248. */
  249.  
  250. log('Initialized', UNIQUE_ID, window.location.href);
  251. })();