Hides chat on YouTube live streams by default
// ==UserScript== // @name YouTube Hide Chat by Default // @namespace https://skoshy.com // @version 0.8.0 // @description Hides chat on YouTube live streams by default // @author Stefan K. // @match https://www.youtube.com/* // @grant GM.getValue // @grant GM.setValue // @icon https://youtube.com/favicon.ico // ==/UserScript== const scriptId = "youtube-hide-chat-by-default"; const CHANNELS_BLOCKLIST = [ // you can place channel IDs here to block them from hiding their chat automatically // example: 'UCTSCjjnCuAPHcfQWNNvULTw' ]; const isInIframe = () => window.top !== window.self; const UNIQUE_ID = (function getUniqueId() { if (isInIframe()) { const capturedUniqueId = new URL(window.location.href).searchParams.get(`${scriptId}-unique-id`); if (!capturedUniqueId) { throw new Error(`Unique ID was not properly passed to iFrame: ${window.location.href}`); } log('Running in an iFrame, grabbed unique ID from URL', capturedUniqueId, window.location.href); return capturedUniqueId; } return Math.floor(Math.random()*1000000); })(); function log(...toLog) { console.log(`[${scriptId}]:`, ...toLog); } const StorageClass = (scriptId, uniqueId, allowedKeys) => { (async function updat###bStorageIds() { const subStorageKey = `${scriptId}_base_subStorageIds`; const subStorageIds = JSON.parse((await GM.getValue(subStorageKey)) || '{}'); console.log({subStorageIds}); await GM.setValue(subStorageKey, JSON.stringify({ ...subStorageIds, [uniqueId]: { dateCreated: Date.now(), }, })); const newSubStorageIds = (await GM.getValue(subStorageKey)) || {}; console.log('Set the value for subStorageIds', newSubStorageIds); })(); const setVal = async (key, val) => { if (!allowedKeys.includes(key)) { throw new Error('Key not allowed'); } await GM.setValue(`${scriptId}_${uniqueId}_${key}`, val); } const getVal = async (key) => { if (!allowedKeys.includes(key)) { throw new Error('Key not allowed'); } return GM.getValue(`${scriptId}_${uniqueId}_${key}`); }; return { setVal, getVal }; }; const { setVal, getVal } = StorageClass(scriptId, UNIQUE_ID, ['lastVidThatHidChat']); (function() { "use strict"; // - if youtube decides to use a new button type, add it here const buttonSelectors = ["button"]; const mutationObserverSelectors = [...buttonSelectors, 'iframe']; function getRootUrlSearchParams() { return new URL(window.location.href).searchParams; } function getCurrentVideoId() { const v = getRootUrlSearchParams().get('v'); if (v) { log('Got Video ID from URL Search Params', v); return v; } if (isInIframe()) { // if not the parent frame, then get it from the passed in iframe url params const passedThroughId = getRootUrlSearchParams().get(`${scriptId}-current-video-id`); log('Not parent frame, getting video ID from passed through ID', passedThroughId); return passedThroughId; } return null; } function getCurrentVideoChannelId() { const channelId = document.querySelector('a[aria-label="About"][href*="channel/"]')?.getAttribute('href')?.match(/\/channel\/(.+)[\/$]/)?.[1]; if (channelId) { return channelId; } if (isInIframe()) { // if not the parent frame, then get it from the passed in iframe url params const passedThroughId = getRootUrlSearchParams().get(`${scriptId}-current-channel-id`); log('Not parent frame, getting channel ID from passed through ID', passedThroughId); if (passedThroughId === 'null' || !passedThroughId) { log('ERROR: There\'s a problem parsing the Channel ID, blocklist functionality will not work', passedThroughId); return null; } return passedThroughId; } return null; } function findAncestorOfElement(el, findFunc) { let currentEl = el; while (currentEl?.parentElement) { const r###lt = findFunc(currentEl.parentElement); if (r###lt) { return currentEl.parentElement; } currentEl = currentEl.parentElement; } return undefined; } function isHideChatButton(node) { const youtubeLiveChatAppAncestor = findAncestorOfElement(node, (parentEl) => { return parentEl.tagName === 'YT-LIVE-CHAT-APP'; }); if (!youtubeLiveChatAppAncestor) { return false; } return (node.getAttribute('aria-label') === 'Close'); } function addedNodeHandler(node) { if (!node.matches) return; if (node.matches('iframe')) { handleAddedIframe(node); return; } if ( !buttonSelectors.some(b => node.matches(b)) ) { return; } if (isHideChatButton(node)) { log(`Found a hide-chat button`, node); const currentVid = getCurrentVideoId(); const currentChannelId = getCurrentVideoChannelId(); const lastVidThatHidChat = getVal('lastVidThatHidChat'); if (lastVidThatHidChat === currentVid) { log(`Already automatically triggered to hide chat for this video`, { lastVidThatHidChat, currentVid, currentChannelId }); return; } if (CHANNELS_BLOCKLIST.includes(currentChannelId)) { log(`Channel in blocklist`, { lastVidThatHidChat, currentVid, currentChannelId }); return; } log(`Attempting to hide the chat by default`, { lastVidThatHidChat, currentVid, currentChannelId }); setVal('lastVidThatHidChat', currentVid); node.click(); } } function handleAddedIframe(node) { if (node.getAttribute(`${scriptId}-modified-src`)) { return; } const url = new URL(node.src); url.searchParams.set(`${scriptId}-unique-id`, UNIQUE_ID); url.searchParams.set(`${scriptId}-current-video-id`, getCurrentVideoId()); url.searchParams.set(`${scriptId}-current-channel-id`, getCurrentVideoChannelId()); log('New iFrame URL', url.toString()); node.src = url.toString(); node.setAttribute(`${scriptId}-modified-src`, true); } /* const bodyObserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { const newNodes = []; mutation.addedNodes.forEach(addedNode => { newNodes.push(addedNode); // it might be text node or comment node which don't have querySelectorAll if (addedNode.querySelectorAll) { mutationObserverSelectors.forEach(bs => { addedNode.querySelectorAll(bs).forEach((n) => { newNodes.push(n); }); }); } }); newNodes.forEach(n => addedNodeHandler(n)); }); }); */ setInterval(() => Array.from( document.querySelectorAll(mutationObserverSelectors.join(', ')) ).forEach(n => addedNodeHandler(n)) , 3000); /* bodyObserver.observe(document, { attributes: true, childList: true, subtree: true, characterData: true }); */ log('Initialized', UNIQUE_ID, window.location.href); })();