Post

Reddit 'On vote move down' userscript

I recently switched from Linux back to Mac. I have actually been a Mac user for many years, but I didn’t like where they were going with the Touch Bar and always chasing to reduce the thickness, often causing issues with usability like with the butterfly keyboard. When I saw that the new MacBook was actually thicker(!) than the previous one, and they removed the Touch Bar I decided to give it another chance.

After switching back I decided to switch from Firefox to Safari as well to maximise battery. Unfortunately the Reddit Enhancement Suite does not work on Safari.

When I started thinking about what I actually needed it for, I managed to reduce it to three things:

  • Voting and moving to the next entry using the keyboard (A to vote up, Z for down)
  • Expanding the content of the active post (using X) and keep it expanded if I move to the next post by voting
  • Dark mode

Fortunately, like most browsers, Safari supports something called user scripts using an extension. Which basically means that I can write some JavaScript to modify a webpage however I like.

Userscripts consist of some metadata in the header comment which specifies the name, description and the rules for execution of the script.

So the script I came up with is the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// ==UserScript==
// @name        OnVoteMoveDownReddit
// @description Simple script that allows using a/z for voting and on vote will move to next element and expand it in Reddit. x will expand the thing if it is not already expanded. When at the end of the page, new content is loaded automatically
// @exclude     https://old.reddit.com/r/*/comments/*
// @exclude     https://www.reddit.com/r/*/comments/*
// @match       https://old.reddit.com/r/*
// @match       https://old.reddit.com/
// @match       https://www.reddit.com/r/*
// @match       https://www.reddit.com/
// ==/UserScript==

// CSS for the active thing
const style = document.createElement("style");
style.type = "text/css";
style.innerHTML =
    ".thing.active { border-style: dotted; border-color: cornflowerblue; }";
document.getElementsByTagName("head")[0].appendChild(style);

// Things are content
const things = document.getElementsByClassName("thing");

const makeActive = (element) => element.classList.add("active");

const makeInactive = (element) => element.classList.remove("active");

const clearPreviousActive = () => {
  for (let thing of things) {
    makeInactive(thing);
  }
};

// Event listeners for activating a thing by clicking on it
for (let thing of things) {
  thing.addEventListener("click", (event) => {
    clearPreviousActive();
    makeActive(event.currentTarget);
  });
}

// Activate first thing by default
makeActive(things[0]);

// Loads next page of 100 things and parses out the list of them
const loadNext = async (lastThing) => {
  const id = lastThing.getAttribute("data-fullname");
  const response = await fetch(
      `${window.location.href.split("?")[0]}?count=100&after=${id}`
  );
  const allContent = await response.text();
  const parser = new DOMParser();
  const htmlDocument = parser.parseFromString(allContent, "text/html");
  const siteTable = htmlDocument.documentElement.querySelector("#siteTable");
  return siteTable.children;
};

// Removes nav buttons and appends new things to the content. New content has its own nav buttons.
const addContentToPage = (lastThing, content) => {
  document.querySelector(".nav-buttons").remove();
  for (let thing of content) {
    lastThing.parentElement.append(thing);
  }
};

const getLastThing = () => {
  const allThings = document.querySelectorAll(".thing");
  return allThings[allThings.length - 1];
};

const loadMoreContent = async () => {
  const lastThing = getLastThing();
  const content = await loadNext(lastThing);
  addContentToPage(lastThing, content);
};

const expand = (thing) => {
  const expandButton = thing.querySelector("div.expando-button.collapsed");
  if (expandButton) {
    expandButton.click();
  }
};

const collapse = (thing) => {
  const collapseButton = thing.querySelector("div.expando-button.expanded");
  if (collapseButton) {
    collapseButton.click();
  }
};

const getNextSibling = (elem, selector) => {
  // Get the next sibling element
  let sibling = elem.nextElementSibling;

  // If the sibling matches our selector, use it
  // If not, jump to the next sibling and continue the loop
  while (sibling) {
    if (sibling.matches(selector)) return sibling;
    sibling = sibling.nextElementSibling;
  }
};

let autoExpand = false;

const moveToNext = (current) => {
  collapse(current);
  // Get next thing that is not an ad
  const nextThing = getNextSibling(current, ".thing:not(.promoted)");
  clearPreviousActive();
  makeActive(nextThing);
  if (autoExpand) {
    expand(nextThing);
  }
  nextThing.scrollIntoView(true);
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
    // you're at the bottom of the page
    loadMoreContent();
  }
};

document.addEventListener(
    "keydown",
    (event) => {
      const keyName = event.key;
      const currentActive = document.querySelector(".thing.active");
      if (currentActive) {
        switch (keyName) {
          case "a":
            const upArrow = currentActive.querySelector(".arrow.up");
            upArrow.click();
            moveToNext(currentActive);
            break;
          case "z":
            const downArrow = currentActive.querySelector(".arrow.down");
            downArrow.click();
            moveToNext(currentActive);
            break;
          case "x":
            autoExpand = !autoExpand;
            if (autoExpand) {
              expand(currentActive);
            } else {
              collapse(currentActive);
            }
            break;
        }
      }
    },
    false
);

I haven’t managed to fix the Dark Mode issue yet because I did not find any good color combinations for the font and the background that work well.

This post is licensed under CC BY 4.0 by the author.