Twitch - Force sort Viewers High to Low

Auto-set sort to "opt1" (Viewers High->Low) with configurable run policy

// ==UserScript==
// @name         Twitch - Force sort Viewers High to Low
// @namespace    https://twitch.tv/
// @version      1.6
// @description  Auto-set sort to "opt1" (Viewers High->Low) with configurable run policy
// @author       Vikindor
// @license      MIT
// @match        https://www.twitch.tv/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // ---------------- CONFIG ----------------
  // RUN_POLICY options:
  //  - 'perTab'  : run once per URL per tab session (F5 won't run again)
  //  - 'perLoad' : run once per URL per page load (F5 will run again)
  const RUN_POLICY = 'perTab'; // If necessary, change this option and save changes

  // Substring that appears in sort dropdown ids/controls
  const SORT_ID_SUBSTR = 'browse-sort-drop-down';

  // The target option id suffix: "opt1" = Viewers (High to Low) in current Twitch UI
  const TARGET_SUFFIX = 'opt1';
  // ---------------------------------------

  // utils
  const waitFor = (selector, { timeout = 15000, interval = 150, filter = null } = {}) =>
    new Promise((resolve, reject) => {
      const t0 = Date.now();
      (function poll() {
        const nodes = Array.from(document.querySelectorAll(selector));
        const el = filter ? nodes.find(filter) : nodes[0];
        if (el) return resolve(el);
        if (Date.now() - t0 > timeout) return reject(new Error('timeout:' + selector));
        setTimeout(poll, interval);
      })();
    });

  const safeClick = (el) => { try { el.click(); } catch (_) {} };

  // ---------------- Defocus helpers ----------------
  // Kills focus on Twitch headings that grab focus on page open (Browse h1, channel title h1, etc.)
  const HEADING_FOCUS_SEL = [
    'h1.tw-title',
    'h1[tabindex="-1"]',
    '[role="heading"].tw-title',
    '[data-test-selector="channel-header-title"] h1',
  ].join(',');

  function defocusWeirdHeading() {
    const el = document.activeElement;
    if (!el || el === document.body) return;
    // Only defocus known heading-like elements
    if (
      el.matches(HEADING_FOCUS_SEL) ||
      ((el.getAttribute('role') === 'heading' || /^H\d$/.test(el.tagName)) && el.tabIndex === -1)
    ) {
      try { el.blur(); } catch (_) {}
    }
  }

  // So outline doesn't flash even for a tick
  (function injectNoOutlineCSS() {
    const css = `
      ${HEADING_FOCUS_SEL}:focus { outline: none !important; box-shadow: none !important; }
    `;
    const style = document.createElement('style');
    style.textContent = css;
    document.documentElement.appendChild(style);
  })();

  // keying
  const urlPart = () => {
    const u = new URL(location.href);
    u.searchParams.delete('sort'); // ignore Twitch sort param
    return `${u.pathname}${u.search}`;
  };
  const loadPart = () => `${performance.timeOrigin}`; // unique per page load

  const keyForUrl = () => {
    if (RUN_POLICY === 'perLoad') return `tw_sort_opt1_${urlPart()}_${loadPart()}`;
    if (RUN_POLICY === 'perTab')  return `tw_sort_opt1_${urlPart()}`;
    return '';
  };

  const alreadyRan = () => !!sessionStorage.getItem(keyForUrl());
  const markRan = () => sessionStorage.setItem(keyForUrl(), '1');

  async function ensureSortOpt1() {
    // If no sort combobox on this page (e.g., channel page), still remove accidental focus
    if (!document.querySelector(`[role="combobox"][aria-controls*="${SORT_ID_SUBSTR}"]`)) {
      defocusWeirdHeading();
      return;
    }
    if (alreadyRan()) return;

    try {
      const combo = await waitFor(
        `[role="combobox"][aria-controls*="${SORT_ID_SUBSTR}"]`
      );

      const current = combo.getAttribute('aria-activedescendant') || '';
      if (current.endsWith(TARGET_SUFFIX)) {
        defocusWeirdHeading();
        markRan();
        return;
      }

      safeClick(combo);
      const option = await waitFor(
        `[id$="${TARGET_SUFFIX}"][role="menuitemradio"], [id$="${TARGET_SUFFIX}"][role="option"], [id$="${TARGET_SUFFIX}"]`,
        { filter: (el) => !!(el.offsetParent || el.getClientRects().length) }
      );
      safeClick(option);

      // Remove focus that sometimes lands on page headings
      setTimeout(defocusWeirdHeading, 0);

      markRan();
    } catch (_) {
      // silent fail
      setTimeout(defocusWeirdHeading, 0);
    }
  }

  // initial run
  setTimeout(() => { defocusWeirdHeading(); ensureSortOpt1(); }, 500);

  // Also catch any later unexpected focus (e.g., Twitch scripts refocus)
  window.addEventListener('focusin', defocusWeirdHeading, true);

  // SPA navigation hook
  (function hookHistory() {
    const fire = () => window.dispatchEvent(new Event('locationchange'));
    const p = history.pushState, r = history.replaceState;
    history.pushState = function () { p.apply(this, arguments); fire(); };
    history.replaceState = function () { r.apply(this, arguments); fire(); };
    window.addEventListener('popstate', fire);
  })();

  window.addEventListener('locationchange', () => {
    setTimeout(() => { defocusWeirdHeading(); ensureSortOpt1(); }, 600);
  });
})();