// ==UserScript==
// @name Reddit Default Sort (Redubbed)
// @namespace https://greasyfork.org/en/users/1510134-blaine-matlock
// @version 2.0
// @description Default Home and subreddits to your preferred sort method
// @author Blaine Matlock
// @license MIT
// @homepageURL https://greasyfork.org/en/scripts/547793-reddit-default-sort-redubbed
// @supportURL https://greasyfork.org/en/scripts/547793-reddit-default-sort-redubbed/feedback
// @match https://www.reddit.com/*
// @match https://reddit.com/*
// @match https://old.reddit.com/*
// @run-at document-start
// @grant none
// ==/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'
// Optional per-subreddit overrides: 'r/SubName': 'new' | 'top' | 'hot' | ...
const SUB_OVERRIDE = {
// 'r/AskReddit': 'new',
// 'r/pcmasterrace': 'top',
};
// Quick toggle (Alt+D) persists in localStorage
const DISABLE_KEY = 'reddit-default-sort:disabled';
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(/\/+$/, '') || '/';
// Always register toggle, even if script is disabled
window.addEventListener('keydown', e => {
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && e.code === 'KeyD') {
const now = localStorage.getItem(DISABLE_KEY) === '1' ? '0' : '1';
localStorage.setItem(DISABLE_KEY, now);
location.reload();
}
}, { capture: true });
if (localStorage.getItem(DISABLE_KEY) === '1') {
return; // disabled via Alt+D
}
// --- Old Reddit fallback (non-SPA) ---
if (location.host === 'old.reddit.com') {
const p = normPath(location.pathname);
const params = new URLSearchParams(location.search);
const isHomeBase = p === '/';
const isSubBase = /^\/r\/[^/]+$/.test(p);
if ((isHomeBase || isSubBase) && !params.has('sort')) {
const sort = isHomeBase
? (HOME_SORTS.includes(DEFAULT_HOME_SORT) ? DEFAULT_HOME_SORT : 'hot')
: (SUB_SORTS.includes(DEFAULT_SUB_SORT) ? DEFAULT_SUB_SORT : 'hot');
params.set('sort', sort);
location.replace(`${location.origin}${location.pathname}?${params.toString()}`);
}
return; // nothing else needed for old reddit
}
// ---- Classifiers (new Reddit SPA) ----
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|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') {
const override = SUB_OVERRIDE[ctx.base]; // 'r/Name'
if (override && SUB_SORTS.includes(override)) return override;
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|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: ensure base routes become sorted (server/app boot in 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);
}
};
// ---- Link rewriting + interception for base forms (ensures subs actually load in default sort) ----
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', 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;
e.preventDefault(); // force real nav so content matches URL/sort
hardAssign(href.toString());
}, { capture: true, passive: false });
});
};
// ---- UI-driven tab fix (if Reddit silently forces Best on base feeds) ----
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'];
default: return [s];
}
};
const textMatch = (el, labels) => {
const t = (el.textContent || '').trim().toLowerCase();
return labels.some(L => t === L.toLowerCase());
};
const findSortElement = (labels, basePrefix) => {
const nodes = Array.from(document.querySelectorAll('a,button,[role="tab"],[role="button"]'));
for (const n of nodes) {
const rect = n.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) continue;
if (!textMatch(n, labels)) continue;
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;
};
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 (explicitSortPresent(ctx)) return; // user explicitly chose a sort
const desired = desiredSortFor(ctx);
const basePrefix = basePrefixFor(ctx);
if (!basePrefix) return;
const labels = labelForSort(desired);
const el = await retry(() => findSortElement(labels, basePrefix), 30, 120);
if (el) {
el.click();
if (desired !== 'new') {
const newEl = await retry(() => findSortElement(labelForSort('new'), basePrefix), 10, 120);
if (newEl) {
setTimeout(() => {
newEl.click();
setTimeout(() => el.click(), 100);
}, 120);
}
}
}
};
// ---- Home/logo rewrite to default Home sort (no preventDefault) ----
const rewriteHomeAnchors = (root = document) => {
const homePath = buildHomePath(HOME_SORTS.includes(DEFAULT_HOME_SORT) ? DEFAULT_HOME_SORT : 'hot');
const anchors = root.querySelectorAll('a[href="/"], a[aria-label="Home"], a[data-testid="reddit-logo"]');
anchors.forEach(a => {
if (a.getAttribute('data-default-sort-home') === '1') return;
a.setAttribute('href', homePath);
a.setAttribute('data-default-sort-home', '1');
a.addEventListener('click', e => {
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const onDefaultHome = normPath(location.pathname) === normPath(homePath);
if (onDefaultHome) {
setTimeout(() => {
try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (_) { window.scrollTo(0, 0); }
}, 0);
}
}, { capture: true, passive: true });
});
};
// ---- Observer with rAF throttling ----
let rafScheduled = false;
const scheduleCheck = () => {
if (rafScheduled) return;
rafScheduled = true;
requestAnimationFrame(() => {
rafScheduled = false;
rewriteAndInterceptBaseLinks();
rewriteHomeAnchors();
ensureTabMatchesUrl();
});
};
const observe = () => {
const mo = new MutationObserver(() => {
scheduleCheck();
});
mo.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href', 'data-testid', 'aria-label']
});
};
// ---- History hooks ----
const hookHistory = () => {
const wrap = name => {
const orig = history[name];
history[name] = function(state, title, url) {
const ret = orig.apply(this, arguments);
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;
}
}
setTimeout(scheduleCheck, 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(scheduleCheck, 50);
});
};
// ---- Boot ----
normalizeFirstLoad(); // before Reddit fully boots
hookHistory();
const start = () => {
scheduleCheck(); // initial run
observe();
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();