// ==UserScript== // @name YouTube - Hide Live Chat By Default // @namespace https://gist.github.com/lbmaian/94824cef728917a53d3c6e6ea885469c // @version 0.15 // @description Hide live chat by default on live streams // @author lbmaian // @match https://www.youtube.com/* // @exclude https://www.youtube.com/embed/* // @noframes // @icon https://www.youtube.com/favicon.ico // @run-at document-start // @grant none // ==/UserScript== (function() { 'use strict'; const DEBUG = false; const logContext = '[YouTube - Hide Live Chat]'; var debug; if (DEBUG) { debug = function(...args) { console.debug(logContext, ...args); } } else { debug = function(...args) {} } function log(...args) { console.log(logContext, ...args); } function warn(...args) { console.warn(logContext, ...args); } function error(...args) { console.error(logContext, ...args); } const EXPANDED_STATE = 'LIVE_CHAT_DISPLAY_STATE_EXPANDED'; const COLLAPSED_STATE = 'LIVE_CHAT_DISPLAY_STATE_COLLAPSED'; let currentPageKey; let initialCollapsePending = true; let fallbackAttempted = false; let fallbackObserver; let fallbackInterval; let fallbackTimeout; function getWatchNextResults(pageData) { return pageData?.response?.contents?.twoColumnWatchNextResults; } function getLiveChatRenderers(pageData) { const watchNextResults = getWatchNextResults(pageData); if (!watchNextResults) { return []; } const renderers = []; const seen = new Set(); const addRenderer = renderer => { if (renderer && !seen.has(renderer)) { seen.add(renderer); renderers.push(renderer); } }; // Legacy watch-page layout. addRenderer(watchNextResults.conversationBar?.liveChatRenderer); // Current watch-page layout. for (const panel of watchNextResults.engagementPanels || []) { addRenderer(panel?.engagementPanelSectionListRenderer?.content?.liveChatRenderer); } return renderers; } function updateChatRenderer(liveChatRenderer, collapsed) { const initialDisplayState = liveChatRenderer.initialDisplayState; const expandedByDefault = initialDisplayState === EXPANDED_STATE || initialDisplayState == null; if (!expandedByDefault || !collapsed) { return false; } log('hiding live chat'); debug('liveChatRenderer.initialDisplayState:', initialDisplayState, '=>', COLLAPSED_STATE); liveChatRenderer.initialDisplayState = COLLAPSED_STATE; const toggleButtonRenderer = liveChatRenderer.showHideButton?.toggleButtonRenderer; if (toggleButtonRenderer) { [toggleButtonRenderer.defaultText, toggleButtonRenderer.toggledText] = [toggleButtonRenderer.toggledText, toggleButtonRenderer.defaultText]; toggleButtonRenderer.isToggled = false; } return true; } function getChatElement() { return document.getElementById('chat') || document.querySelector('ytd-live-chat-frame#chat'); } function shouldCollapseCurrentChat() { const chat = getChatElement(); // Keep an already-open chat open when navigating between videos. return !chat || chat.collapsed !== false; } function isWatchPage() { return location.pathname === '/watch' || location.pathname.startsWith('/live/') || location.pathname.startsWith('/clip/'); } function getPageKey(pageData) { return pageData?.videoDetails?.videoId || pageData?.response?.videoDetails?.videoId || pageData?.response?.currentVideoEndpoint?.watchEndpoint?.videoId || new URL(location.href).searchParams.get('v') || location.href; } function getButtonLabel(button) { return [ button.getAttribute('aria-label'), button.getAttribute('title'), button.textContent ].filter(Boolean).join(' ').trim().toLowerCase(); } function isShowButton(button) { return /show\s+(?:live\s+)?chat|show chat replay|显示.*聊天|显示.*回放|チャットを表示|mostrar.*chat/i .test(getButtonLabel(button)); } function findButton(root, selector) { const closeButton = root.querySelector('#close-button button') || root.querySelector('#close-button'); if (closeButton) { return closeButton; } const toggleButton = root.querySelector('#show-hide-button button') || root.querySelector('#show-hide-button'); if (toggleButton && !isShowButton(toggleButton)) { return toggleButton; } const buttons = root.querySelectorAll(selector); return Array.from(buttons).find(button => !isShowButton(button)); } function findChatButtonInDocument() { const chatRoot = getChatElement(); if (!chatRoot || chatRoot.collapsed === true) { return null; } return findButton(chatRoot, '#chat-toggle-button'); } function findChatButtonInFrame() { const frame = document.querySelector('iframe#chatframe'); if (!frame || getChatElement()?.collapsed === true) { return null; } try { const frameDocument = frame.contentDocument || frame.contentWindow?.document; return frameDocument && findButton(frameDocument, '#show-hide-button button, #show-hide-button'); } catch (error) { debug('chat iframe is not accessible yet', error); return null; } } function findChatButton() { return findChatButtonInDocument() || findChatButtonInFrame(); } function clickChatButton(button) { try { button.click(); } catch (error) { debug('button.click() failed, dispatching click event', error); button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } } function stopFallback() { fallbackObserver?.disconnect(); fallbackObserver = undefined; clearInterval(fallbackInterval); fallbackInterval = undefined; clearTimeout(fallbackTimeout); fallbackTimeout = undefined; } function collapseChatFromDom() { const chat = getChatElement(); if (chat?.collapsed === true) { return true; } const button = findChatButton(); if (!button) { return false; } log('hiding live chat using DOM fallback'); clickChatButton(button); fallbackAttempted = true; return true; } function startDomFallback() { if (fallbackAttempted || fallbackObserver || !isWatchPage()) { return; } if (collapseChatFromDom()) { return; } const tryCollapse = () => { if (fallbackAttempted || collapseChatFromDom()) { stopFallback(); } }; fallbackObserver = new MutationObserver(tryCollapse); fallbackObserver.observe(document.documentElement || document, { childList: true, subtree: true }); fallbackInterval = setInterval(tryCollapse, 500); fallbackTimeout = setTimeout(stopFallback, 15000); } function resetForPage(pageData) { const pageKey = getPageKey(pageData); if (pageKey !== currentPageKey) { stopFallback(); fallbackAttempted = false; currentPageKey = pageKey; } } function handlePageData(pageData = {}) { resetForPage(pageData); if (!isWatchPage()) { return; } // On the first watch page, an already-created open chat is still the // default state, not evidence that the user opened it. const collapsed = initialCollapsePending || shouldCollapseCurrentChat(); const renderers = getLiveChatRenderers(pageData); if (renderers.length) { const changed = renderers.some(renderer => updateChatRenderer(renderer, collapsed)); if (collapsed && (changed || getChatElement()?.collapsed === false)) { // The iframe can be created after page data is fetched. startDomFallback(); } if (changed || getChatElement()?.collapsed === true || fallbackAttempted) { initialCollapsePending = false; } return; } // Continue to support pages whose live-chat renderer is not exposed in // the page-data object used by the current experiment. if (collapsed) { startDomFallback(); if (getChatElement()?.collapsed === true || fallbackAttempted) { initialCollapsePending = false; } } } function getEventPageData(event) { return event?.detail?.pageData || event?.detail?.detail?.pageData || (event?.detail?.response ? event.detail : undefined); } function handlePageEvent(event) { handlePageData(getEventPageData(event) || { response: window.ytInitialData || {} }); } // YouTube replaces the watch page through SPA navigation. Capture both // early and late page-data events because their payloads vary by rollout. for (const eventName of [ 'yt-page-data-fetched', 'yt-page-data-updated', 'yt-navigate-finish' ]) { window.addEventListener(eventName, handlePageEvent, true); } window.addEventListener('yt-navigate-start', () => { stopFallback(); fallbackAttempted = false; currentPageKey = undefined; }, true); function handleInitialPage() { if (window.ytInitialData) { handlePageData({ response: window.ytInitialData }); } else if (isWatchPage() && initialCollapsePending) { // Some userscript managers expose a separate window world where // ytInitialData is unavailable. The DOM fallback still works. startDomFallback(); if (getChatElement()?.collapsed === true || fallbackAttempted) { initialCollapsePending = false; } } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', handleInitialPage, { once: true }); } else { handleInitialPage(); } window.addEventListener('pageshow', event => { if (event.persisted) { handleInitialPage(); } }, true); })();