// ==UserScript==
// @name [Lemmy] Sort Posts, Comments, Communities & Search
// @match https://aussie.zone/*
// @match https://beehaw.org/*
// @match https://discuss.tchncs.de/*
// @match https://feddit.nl/*
// @match https://feddit.org/*
// @match https://feddit.uk/*
// @match https://hexbear.net/*
// @match https://infosec.pub/*
// @match https://jlai.lu/*
// @match https://lemmy.blahaj.zone/*
// @match https://lemmy.ca/*
// @match https://lemmy.dbzer0.com/*
// @match https://lemmy.ml/*
// @match https://lemmy.today/*
// @match https://lemmy.world/*
// @match https://lemmy.zip/*
// @match https://lemmybefree.net/*
// @match https://lemmygrad.ml/*
// @match https://midwest.social/*
// @match https://programming.dev/*
// @match https://reddthat.com/*
// @match https://sh.itjust.works/*
// @match https://slrpnk.net/*
// @match https://sopuli.xyz/*
// @noframes
// @run-at document-start
// @inject-into page
// @grant GM_deleteValue
// @grant GM_getValues
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant unsafeWindow
// @namespace Violentmonkey Scripts
// @author SedapnyaTidur
// @version 1.0.0
// @license MIT
// @revision 8/18/2025, 11:39:49 PM
// @description Sort Lemmy posts, comments, communities & search. Reload the webpage after changing the sort type in menu to take effect. To make this script runnable, the CSP for the website must be disabled/modified/removed using an addon.
// ==/UserScript==
(function() {
'use strict';
const posts = {
Hot: 'Hot',
Active: 'Active',
Scaled: 'Scaled',
Controversial: 'Controversial',
New: 'New',
Old: 'Old',
MostComments: 'Most Comments',
NewComments: 'New Comments',
TopHour: 'Top Hour',
TopSixHour: 'Top 6 Hours',
TopTwelveHour: 'Top 12 Hours',
TopDay: 'Top Day',
TopWeek: 'Top Week',
TopMonth: 'Top Month',
TopThreeMonths: 'Top 3 Months',
TopSixMonths: 'Top 6 Months',
TopNineMonths: 'Top 9 Months',
TopYear: 'Top Year',
TopAll: 'Top All Time'
};
const comments = ['Hot', 'Top', 'Controversial', 'New', 'Old'];
// Based on the keys of "posts" above and not the values for posts, communities & search.
const defaults = {
posts: 'Active',
comments: 'Hot',
communities: 'TopMonth',
search: 'TopAll'
};
let { postsSortBy, commentsSortBy, communitiesSortBy, searchSortBy, reload } = GM_getValues({ postsSortBy: defaults.posts, commentsSortBy: defaults.comments, communitiesSortBy: defaults.communities, searchSortBy: defaults.search, reload: false });
const window = unsafeWindow;
let attrObserver, currURL = window.location.href, first = true;
let searchInterval, searchTimeout, startInterval, startTimeout;
const configs = [{
path: /^(?:\/$|\/c\/)/,
defaultSort: defaults.posts,
sort: postsSortBy
}, {
path: /^\/post\//,
defaultSort: defaults.comments,
sort: commentsSortBy
}, {
path: /^\/communities/,
defaultSort: defaults.communities,
sort: communitiesSortBy
}, {
path: /^\/search/,
defaultSort: defaults.search,
sort: searchSortBy
}];
if (reload) GM_deleteValue('reload');
const location = window.location;
// For URL like this "https://lemmy.ca/search?" remove the "?" at the end and window.location.search is empty.
const href = location.href.replace(/([^=])\?+$/, '$1');
const path = location.pathname;
const search = location.search;
// May redirect to a different URL for the first visit.
for (const config of configs) {
if (config.path.test(path)) {
if (!search) { //window.location.search is empty.
if (config.sort !== config.defaultSort) {
window.stop();
location.replace(href + `?sort=${config.sort}`);
return;
}
} else if (/[?&]sort=[^&]+/.test(search)) {
if (!reload && !search.includes(`sort=${config.sort}`)) {
window.stop();
location.replace(href.replace(/(.)sort=[^&]+/, `$1sort=${config.sort}`));
return;
}
} else {
window.stop();
location.replace(href + `&sort=${config.sort}`);
return;
}
}
}
// "inject-into page" is a must for this to work.
const pushState = window.History.prototype.pushState;
window.History.prototype.pushState = function() {
const location = new URL(arguments[2], window.location.href);
const href = location.href;
const path = location.pathname;
const search = location.search;
for (const config of configs) {
if (config.path.test(path)) {
if (!search) { // Consume if window.location.search is empty.
if (config.sort !== config.defaultSort) {
arguments[2] = href + `?sort=${config.sort}`;
}
} else if (!/[?&]sort=[^&]+/.test(search)) { // So that users can change to different sort types.
arguments[2] = href + `&sort=${config.sort}`;
}
// Avoid duplicate URLs in history when clicking the top-left icon/label.
if(arguments[2] === window.location.href && window.location.pathname === '/') return window.history.go(0);
break;
}
}
return pushState.apply(this, arguments);
};
// Dirty trick to make the visited posts highlighted via a style a:visited.
const changeLinks = function() {
searchTimeout = setTimeout(() => {
clearInterval(searchInterval);
}, 5000);
searchInterval = setInterval(() => {
const targets = document.body.querySelectorAll('a[href^="/post/"]');
if (targets.length < 6) return;
clearInterval(searchInterval);
clearTimeout(searchTimeout);
searchInterval = 0;
searchTimeout = 0;
for (const anchor of targets) {
const href = anchor.href; // Complete URL.
const search = href.replace(/^[^?]+/, '');
if (!search) {
if (commentsSortBy !== defaults.comments) {
anchor.href = href + `?sort=${commentsSortBy}`;
}
} else if (!/[?&]sort=[^&]+/.test(search)) {
anchor.href = href + `&sort=${commentsSortBy}`;
}
if (first) { // May get overriden by Lemmy.
first = false;
attrObserver = new MutationObserver(changeLinks);
attrObserver.observe(anchor, { attributes: true });
}
}
}, 500);
};
const reset = function() {
clearInterval(searchInterval);
clearTimeout(searchTimeout);
if (attrObserver) {
attrObserver.disconnect();
attrObserver = undefined;
}
searchInterval = 0;
searchTimeout = 0;
first = true;
};
const start = function() {
startTimeout = setTimeout(() => {
clearInterval(startInterval);
}, 5000);
startInterval = setInterval(() => {
if (!document.body) return;
clearInterval(startInterval);
clearTimeout(startTimeout);
new MutationObserver(() => {
if (window.location.href === currURL) return;
currURL = window.location.href;
reset();
if (/^(?:\/$|\/c\/|\/search)/.test(window.location.pathname)) {
changeLinks();
return;
}
}).observe(document.body, { childList: true, subtree: true });
}, 500);
};
// Change visited links for the first time or reload.
if (/^(?:\/$|\/c\/|\/search)/.test(window.location.pathname)) changeLinks();
start();
window.addEventListener('beforeunload', () => {
GM_setValue('reload', true);
}, false);
const next = function(array, startIndex, sortBy) {
for (let i = startIndex; i < array.length; ++i) {
if (array[i] === sortBy) {
if (i === array.length - 1) return array[startIndex];
return array[i + 1];
}
}
};
const clickPosts = function() {
postsSortBy = next(Object.keys(posts), 0, postsSortBy);
GM_setValue('postsSortBy', postsSortBy);
GM_registerMenuCommand(`Posts: 《${posts[postsSortBy]}》`, clickPosts, { id: '0', autoClose: false, title: 'Click to change the posts sort type.' });
};
const clickComments = function() {
commentsSortBy = next(comments, 0, commentsSortBy);
GM_setValue('commentsSortBy', commentsSortBy);
GM_registerMenuCommand(`Comments: 《${commentsSortBy}》`, clickComments, { id: '1', autoClose: false, title: 'Click to change the comments sort type.' });
};
const clickCommunities = function() {
communitiesSortBy = next(Object.keys(posts), 0, communitiesSortBy);
GM_setValue('communitiesSortBy', communitiesSortBy);
GM_registerMenuCommand(`Communities: 《${posts[communitiesSortBy]}》`, clickCommunities, { id: '2', autoClose: false, title: 'Click to change the communities sort type.' });
};
const clickSearch = function() {
searchSortBy = next(Object.keys(posts), 3, searchSortBy);
GM_setValue('searchSortBy', searchSortBy);
GM_registerMenuCommand(`Search: 《${posts[searchSortBy]}》`, clickSearch, { id: '3', autoClose: false, title: 'Click to change the search sort type.' });
};
GM_registerMenuCommand(`Posts:《${posts[postsSortBy]}》`, clickPosts, { id: '0', autoClose: false, title: 'Click to change the posts sort type.' });
GM_registerMenuCommand(`Comments:《${commentsSortBy}》`, clickComments, { id: '1', autoClose: false, title: 'Click to change the comments sort type.' });
GM_registerMenuCommand(`Communities: 《${posts[communitiesSortBy]}》`, clickCommunities, { id: '2', autoClose: false, title: 'Click to change the communities sort type.' });
GM_registerMenuCommand(`Search: 《${posts[searchSortBy]}》`, clickSearch, { id: '3', autoClose: false, title: 'Click to change the search sort type.' });
})();