Last active 1 month ago

sometimes when a post is really big trying to manage it via inbox is futile. now also checks your threads for AC viability + teammate threads

Revision 01d4145aad79fe5b954bc004533a9090a5711c3f

newcomments.user.js Raw
1// ==UserScript==
2// @name Highlight New Replies
3// @namespace http://veryroundbird.house
4// @version 2026-06-03
5// @description highlights your threads where you're not the most recent reply.
6// @author Carly Smallbird
7// @match https://*.dreamwidth.org/*
8// @icon https://www.google.com/s2/favicons?sz=64&domain=dreamwidth.org
9// @grant none
10// ==/UserScript==
11
12(function() {
13 'use strict';
14 let readReplies = [];
15 let linkList = [];
16 const username = document.querySelector('#account-links-text .ljuser').getAttribute('lj:user');
17 const newReplyText = "✨ NEW REPLIES ✨";
18 const markDoneText = "Mark Thread Done";
19 const highlightColor = "yellow";
20 const jumpLinks = document.createElement('div');
21 const jumpLinksOl = document.createElement('ol');
22 jumpLinks.append(jumpLinksOl);
23 jumpLinks.id = 'jump-links';
24 document.body.append(jumpLinks);
25
26 const addReadReply = (id) => {
27 if (readReplies.indexOf(parseInt(id)) === -1) {
28 readReplies.push(parseInt(id));
29 localStorage.setItem("readComments", JSON.stringify(readReplies));
30 }
31 };
32
33 const highlightCmt = (cmt, top) => {
34 const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", ""));
35 const id = parseInt(top.querySelector('.comment').id.replace("comment-cmt", ""));
36 const info = top.querySelector('.comment-info');
37 const markReadBtn = document.createElement('button');
38 let readListener = null;
39 markReadBtn.setAttribute('type', 'button');
40 markReadBtn.classList.add("markRead");
41 markReadBtn.textContent = newReplyText;
42 top.classList.add('highlight');
43
44 if (cmt.querySelector('.partial')) {
45 top.querySelector('.comment .inner').append(markReadBtn);
46 readListener = top.querySelector('a[onclick]').addEventListener('click', (_e) => {
47 markReadBtn.remove();
48 setTimeout(() => {
49 top.querySelector('.comment-info h4').append(markReadBtn);
50 }, 1000);
51 });
52 } else {
53 top.querySelector('.comment-info h4').append(markReadBtn);
54 }
55
56 markReadBtn.addEventListener('click', (e) => {
57 e.preventDefault();
58 addReadReply(id);
59 top.classList.remove("highlight");
60 markReadBtn.remove();
61 jumpLinks.querySelector(`a[href="#cmt${id}"]`).remove();
62 if (readListener) {
63 removeEventListener('click', readListener);
64 }
65 });
66 };
67
68 const main = () => {
69 const newStyles = document.createElement('style');
70 newStyles.setAttribute('type', 'text/css');
71 newStyles.textContent = `
72 .highlight .partial > .comment,
73 .highlight .visible .comment-info {
74 background-color: ${highlightColor} !important;
75 }
76
77 button.markRead {
78 padding: 2px;
79 margin-bottom: 0;
80 font-size: .8em;
81 }
82
83 .comment .inner > button.markRead {
84 margin-left: 5px;
85 }
86
87 #jump-links {
88 position: fixed;
89 bottom: 10px;
90 left: 10px;
91 padding: 5px;
92 border: 1px gray solid;
93 background-color: #FFFFFF66;
94 max-height: 150px;
95 max-width: 200px;
96 overflow: auto;
97 opacity: .5;
98 }
99
100 #jump-links ol {
101 font-size: .8em;
102 margin-bottom: 0;
103 }
104
105 #jump-links:hover,
106 #jump-links:focus-within {
107 opacity: 1;
108 }
109 `;
110 document.head.append(newStyles);
111
112 readReplies = localStorage.getItem("readComments") ? JSON.parse(localStorage.getItem("readComments")) : [];
113 let prevDepth = 1;
114 let topOfThread = null;
115 let prevComment = null;
116 document.querySelectorAll('.comment-thread').forEach((cmt) => {
117 const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", ""));
118 if (!topOfThread || depth === 1) {
119 if (!cmt.querySelector('.comment-wrapper').classList.contains(`poster-${username}`)) {
120 prevComment = null;
121 return;
122 }
123 topOfThread = cmt;
124 prevComment = cmt;
125 return;
126 }
127 if (depth >= prevDepth) {
128 prevComment = cmt;
129 prevDepth = depth;
130 return;
131 }
132 if (depth < prevDepth) {
133 const id = parseInt(topOfThread.querySelector('.comment').id.replace("comment-cmt", ""));
134 if (prevComment && !prevComment.querySelector('.comment-wrapper').classList.contains(`poster-${username}`) && readReplies.indexOf(id) === -1) {
135 highlightCmt(cmt, topOfThread);
136 linkList.push(id);
137 }
138 topOfThread = null;
139 prevComment = null;
140 prevDepth = 1;
141 return;
142 }
143 prevComment = cmt;
144 prevDepth = depth;
145 });
146
147 linkList.forEach((id) => {
148 const newLi = document.createElement('li');
149 const newA = document.createElement('a');
150 newA.href = `#cmt${id}`;
151 newA.textContent = `#${id}`;
152 newLi.append(newA);
153 jumpLinksOl.append(newLi);
154 });
155 };
156
157 main();
158})();