Reddit Default Sort ReDubbed

Default Home and all subreddits to your chosen sort method

Versão de: 30/08/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Reddit Default Sort ReDubbed
// @namespace    force-sort-reddit
// @version      1.8
// @description  Default Home and all subreddits to your chosen sort method
// @match        https://www.reddit.com/*
// @match        https://reddit.com/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  // === Config ===
  const DEFAULT_HOME_SORT = 'hot'; // 'hot','new','top','rising','best'
  const DEFAULT_SUB_SORT = 'hot'; // 'hot','new','top','rising','controversial','gilded','best'

  const HOME_SORTS = ['hot', 'new', 'top', 'rising', 'best'];
  const SUB_SORTS = ['hot', 'new', 'top', 'rising', 'controversial', 'gilded', 'best'];
  const ALL_SORTS = Array.from(new Set([...HOME_SORTS, ...SUB_SORTS]));

  const normPath = p => (p || '/').replace(/\/+$/, '') || '/';

  // ---- Classifiers ----
  const isKnownNonFeed = p => {
    p = normPath(p);
    return /^\/(message|settings|user|u|chat|topics|mod|poll|subreddits|coins|premium|help|appeal|rules|policies)\b/.test(p) ||
           /^\/search\b/.test(p);
  };

  const isHomeBase = p => normPath(p) === '/';
  const isHomeSorted = p => /^\/(best|hot|new|top|rising)$/.test(normPath(p));
  const isAllPopBase = p => /^\/r\/(all|popular)$/.test(normPath(p));
  const isAllPopSorted = p => /^\/r\/(all|popular)\/(best|hot|new|top|rising)$/.test(normPath(p));
  const isSubBase = p => /^\/r\/[^/]+$/.test(normPath(p));
  const isSubSorted = p => /^\/r\/[^/]+\/(best|hot|new|top|rising|controversial|gilded)$/.test(normPath(p));

  const getContext = () => {
    const p = normPath(location.pathname);
    if (isKnownNonFeed(p)) return { type: 'non' };
    if (isHomeBase(p)) return { type: 'home-base' };
    if (isHomeSorted(p)) return { type: 'home-sorted', sort: p.slice(1) };
    if (isAllPopBase(p)) {
      const base = p.split('/').filter(Boolean).slice(0, 2).join('/'); // r/all or r/popular
      return { type: 'allpop-base', base };
    }
    if (isAllPopSorted(p)) {
      const seg = p.split('/').filter(Boolean);
      return { type: 'allpop-sorted', base: seg.slice(0, 2).join('/'), sort: seg[2] };
    }
    if (isSubBase(p)) {
      const base = p.split('/').filter(Boolean).slice(0, 2).join('/'); // r/<name>
      return { type: 'sub-base', base };
    }
    if (isSubSorted(p)) {
      const seg = p.split('/').filter(Boolean);
      return { type: 'sub-sorted', base: seg.slice(0, 2).join('/'), sort: seg[2] };
    }
    return { type: 'other' };
  };

  const desiredSortFor = ctx => {
    if (ctx.type === 'home-base' || ctx.type === 'home-sorted' ||
        ctx.type === 'allpop-base' || ctx.type === 'allpop-sorted') {
      return HOME_SORTS.includes(DEFAULT_HOME_SORT) ? DEFAULT_HOME_SORT : 'hot';
    }
    if (ctx.type === 'sub-base' || ctx.type === 'sub-sorted') {
      return SUB_SORTS.includes(DEFAULT_SUB_SORT) ? DEFAULT_SUB_SORT : 'hot';
    }
    return 'hot';
  };

  const basePrefixFor = ctx => {
    if (ctx.type.startsWith('home')) return '/';
    if (ctx.type.startsWith('allpop')) return '/' + ctx.base + '/';
    if (ctx.type.startsWith('sub')) return '/' + ctx.base + '/';
    return null;
  };

  const explicitSortPresent = ctx => ctx.type.endsWith('sorted');

  // ---- URL builders ----
  const buildHomePath = (sort) => '/' + (HOME_SORTS.includes(sort) ? sort : 'hot') + '/';
  const buildAllPopPath = (base, sort) => '/' + base + '/' + (HOME_SORTS.includes(sort) ? sort : 'hot') + '/';
  const buildSubredditPath = (base, sort) => '/' + base + '/' + (SUB_SORTS.includes(sort) ? sort : 'hot') + '/';

  const toDefaultSortedUrl = url => {
    const u = new URL(url, location.origin);
    const p = normPath(u.pathname);
    if (isKnownNonFeed(p)) return url;

    if (isHomeBase(p)) {
      u.pathname = buildHomePath(desiredSortFor({ type: 'home-base' }));
      u.search = '';
      return u.toString();
    }
    if (isAllPopBase(p)) {
      const base = p.split('/').filter(Boolean).slice(0, 2).join('/'); // r/all or r/popular
      u.pathname = buildAllPopPath(base, desiredSortFor({ type: 'allpop-base' }));
      u.search = '';
      return u.toString();
    }
    if (isSubBase(p)) {
      const base = p.split('/').filter(Boolean).slice(0, 2).join('/'); // r/<name>
      u.pathname = buildSubredditPath(base, desiredSortFor({ type: 'sub-base' }));
      u.search = '';
      return u.toString();
    }
    return url;
  };

  // ---- Hard navigation helpers ----
  const hardAssign = (url) => { location.assign(url); };
  const hardReplace = (url) => { location.replace(url); };

  // ---- First-load: hard-replace base routes so the server/app boots in the right sort ----
  const normalizeFirstLoad = () => {
    const p = normPath(location.pathname);
    if (isKnownNonFeed(p)) return;
    if (isHomeBase(p) || isAllPopBase(p) || isSubBase(p)) {
      const target = toDefaultSortedUrl(location.href);
      if (target !== location.href) hardReplace(target);
    }
  };

  // ---- Subreddit/Home link rewriting + interception for base forms ----
  const rewriteAndInterceptBaseLinks = (root = document) => {
    const anchors = root.querySelectorAll('a[href], area[href]');
    anchors.forEach(a => {
      if (a.getAttribute('data-sort-rewritten') === '1') return;
      const raw = a.getAttribute('href');
      if (!raw) return;

      let href;
      try { href = new URL(raw, location.origin); } catch { return; }
      const p = normPath(href.pathname);

      // Respect explicit sorts anywhere
      if (isHomeSorted(p) || isAllPopSorted(p) || isSubSorted(p)) return;

      // Base forms: rewrite href to default-sorted and hard-navigate on left-click
      if (isHomeBase(p)) {
        href.pathname = buildHomePath(desiredSortFor({ type: 'home-base' }));
      } else if (isAllPopBase(p)) {
        const base = p.split('/').filter(Boolean).slice(0, 2).join('/'); // r/all|popular
        href.pathname = buildAllPopPath(base, desiredSortFor({ type: 'allpop-base' }));
      } else if (isSubBase(p)) {
        const base = p.split('/').filter(Boolean).slice(0, 2).join('/'); // r/<name>
        href.pathname = buildSubredditPath(base, desiredSortFor({ type: 'sub-base' }));
      } else {
        return;
      }

      href.search = '';
      a.setAttribute('href', href.toString());
      a.setAttribute('data-sort-rewritten', '1');

      a.addEventListener('click', e => {
        if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
        // Important: force a full navigation so Reddit actually loads the chosen sort
        e.preventDefault();
        hardAssign(href.toString());
      }, { capture: true, passive: false });
    });
  };

  // ---- UI-driven tab fix (last resort if Reddit still shows Best) ----
  const labelForSort = (s) => {
    switch (s) {
      case 'hot': return ['Hot'];
      case 'new': return ['New'];
      case 'top': return ['Top'];
      case 'rising': return ['Rising'];
      case 'best': return ['Best'];
      case 'controversial': return ['Controversial'];
      case 'gilded': return ['Gilded', 'Awarded']; // Reddit sometimes labels as "Awarded"
      default: return [s];
    }
  };

  const textMatch = (el, labels) => {
    const t = (el.textContent || '').trim().toLowerCase();
    return labels.some(L => t === L.toLowerCase());
  };

  const findSortElement = (labels, basePrefix) => {
    // 1) Prefer obvious tabs/links near the target basePrefix
    const nodes = Array.from(document.querySelectorAll('a,button,[role="tab"],[role="button"]'));
    for (const n of nodes) {
      // Must be visible-ish
      const rect = n.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) continue;
      // Match by label text
      if (!textMatch(n, labels)) continue;

      // If it's an <a>, ensure it points to same feed base with /<sort>/ in path
      if (n.tagName === 'A') {
        try {
          const u = new URL(n.getAttribute('href'), location.origin);
          const ap = normPath(u.pathname);
          if (!ap.startsWith(normPath(basePrefix))) continue;
        } catch { continue; }
      }
      return n;
    }
    return null;
  };

  // Simple retry helper
  const retry = (fn, tries = 20, delay = 100) => new Promise(resolve => {
    const attempt = () => {
      const val = fn();
      if (val) resolve(val);
      else if (--tries > 0) setTimeout(attempt, delay);
      else resolve(null);
    };
    attempt();
  });

  const ensureTabMatchesUrl = async () => {
    const ctx = getContext();
    if (ctx.type === 'non' || ctx.type === 'other') return;

    // If the URL already has an explicit sort, respect it and do nothing.
    if (explicitSortPresent(ctx)) return;

    // Otherwise pick desired default for this context
    const desired = desiredSortFor(ctx);
    const basePrefix = basePrefixFor(ctx);
    if (!basePrefix) return;

    // Wait for sort controls to render, then click the tab/button by label
    const labels = labelForSort(desired);
    const el = await retry(() => findSortElement(labels, basePrefix), 30, 120);
    if (el) {
      el.click();
      // In case Reddit ignores same-route click, bounce via "New" then back.
      if (desired !== 'new') {
        const newEl = await retry(() => findSortElement(labelForSort('new'), basePrefix), 10, 120);
        if (newEl) {
          setTimeout(() => {
            newEl.click();
            setTimeout(() => el.click(), 100);
          }, 120);
        }
      }
    }
  };

  // ---- Observe mounts and route changes ----
  const observe = () => {
    const mo = new MutationObserver(muts => {
      for (const m of muts) {
        if (m.type === 'attributes' && m.target && (m.attributeName === 'href' || m.attributeName === 'data-testid')) {
          rewriteAndInterceptBaseLinks(m.target);
        }
        for (const node of m.addedNodes) {
          if (node.nodeType === 1) {
            rewriteAndInterceptBaseLinks(node);
          }
        }
      }
      // After DOM changes, try to ensure correct sort on base feeds.
      ensureTabMatchesUrl();
    });
    mo.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['href', 'data-testid', 'aria-label']
    });
  };

  // ---- History hooks: after SPA navigations, verify sort via tab click if needed ----
  const hookHistory = () => {
    const wrap = name => {
      const orig = history[name];
      history[name] = function(state, title, url) {
        const ret = orig.apply(this, arguments);
        // If Reddit routes to a base path, do a hard redirect to sorted URL immediately.
        const abs = url ? new URL(url, location.href).toString() : location.href;
        const p = normPath(new URL(abs).pathname);
        if (!isKnownNonFeed(p) && (isHomeBase(p) || isAllPopBase(p) || isSubBase(p))) {
          const target = toDefaultSortedUrl(abs);
          if (target !== abs) {
            hardAssign(target);
            return ret;
          }
        }
        // Otherwise, try UI-driven fix (e.g., if Reddit silently forced Best)
        setTimeout(ensureTabMatchesUrl, 50);
        return ret;
      };
    };
    wrap('pushState');
    wrap('replaceState');
    window.addEventListener('popstate', () => {
      const p = normPath(location.pathname);
      if (!isKnownNonFeed(p) && (isHomeBase(p) || isAllPopBase(p) || isSubBase(p))) {
        const target = toDefaultSortedUrl(location.href);
        if (target !== location.href) {
          hardAssign(target);
          return;
        }
      }
      setTimeout(ensureTabMatchesUrl, 50);
    });
  };

  // ---- Boot ----
  normalizeFirstLoad(); // before Reddit fully boots
  hookHistory();
  const start = () => {
    rewriteAndInterceptBaseLinks();
    ensureTabMatchesUrl();
    observe();
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start);
  } else {
    start();
  }
})();