Greasy Fork is available in English.

YouTube Hide Chat by Default

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 updateSubStorageIds() {
    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 result = findFunc(currentEl.parentElement);

      if (result) {
        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);
})();