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 d0a313a7f058761a71f8466c03742eb4f65e18cc

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// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
10// @grant GM_getValue
11// @grant GM_setValue
12// @grant GM.getValue
13// @grant GM.setValue
14// ==/UserScript==
15
16// make the settings
17let gmc = new GM_config(
18{
19 'id': 'veryroundbirdhouse_newreplies',
20 'title': 'Settings',
21 'fields':
22 {
23 'newReplyText':
24 {
25 'label': 'New Reply Text',
26 'type': 'text',
27 'default': '✨ NEW REPLIES ✨'
28 },
29 'markDoneText':
30 {
31 'label': 'Mark Thread Done Text',
32 'type': 'text',
33 'default': '✅ Mark Thread Done'
34 },
35 'highlightColor':
36 {
37 'label': 'Highlight Color',
38 'type': 'text',
39 'default': 'yellow'
40 }
41 },
42 'init': () => {
43 gmc.css.basic = `
44 label {
45 display: block;
46 }
47 `;
48 }
49});
50
51(function() {
52 if (window.location.href.match(/https:\/\/[a-z\-]+\.dreamwidth.org\/(?:[0-9]+|(?:[0-9]{4}\/[0-9]{2}\/[0-9]{2}\/[a-z\-]+))\.html/)) { // checks that this is like, an actual entry
53 'use strict';
54
55 // set up globals
56 let readReplies = [];
57 let linkList = [];
58
59 // get username from account links
60 const username = document.querySelector('#account-links-text .ljuser').getAttribute('lj:user');
61
62 // initialize user settings
63 const newReplyText = "✨ NEW REPLIES ✨";
64 const markDoneText = "✅ Mark Thread Done";
65 const highlightColor = "yellow";
66
67 // create elements for jump links menu
68 const jumpLinks = document.createElement('div');
69 const jumpDetails = document.createElement('details');
70 const jumpSummary = document.createElement('summary');
71 const jumpLinksOl = document.createElement('ol');
72 const settingsLink = document.createElement('a');
73 jumpSummary.textContent = "🧵 New Comments";
74 jumpDetails.append(jumpSummary);
75 jumpDetails.append(jumpLinksOl);
76 jumpLinks.append(jumpDetails);
77 settingsLink.textContent = "⚙️ Settings";
78 settingsLink.href = "#";
79 settingsLink.addEventListener('click', (e) => {
80 e.preventDefault();
81 gmc.open();
82 });
83 jumpLinks.append(settingsLink);
84 jumpLinks.id = 'jump-links';
85 document.body.append(jumpLinks);
86
87 // add an item to the list of threads we're ignoring
88 const addReadReply = (id) => {
89 if (readReplies.indexOf(parseInt(id)) === -1) {
90 readReplies.push(parseInt(id));
91 localStorage.setItem("readComments", JSON.stringify(readReplies));
92 }
93 };
94
95 // adds UI elements to highlighted comments
96 const highlightCmt = (cmt, top) => {
97 const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", ""));
98 const id = parseInt(top.querySelector('.comment').id.replace("comment-cmt", ""));
99 const info = top.querySelector('.comment-info');
100 const markReadBtn = document.createElement('button');
101 const newCmtMarker = document.createElement('span');
102 let expandListener = null;
103 markReadBtn.setAttribute('type', 'button');
104 markReadBtn.classList.add("markRead");
105 markReadBtn.textContent = markDoneText;
106 newCmtMarker.textContent = newReplyText;
107 newCmtMarker.classList.add('newComment');
108 top.classList.add('highlight');
109
110 const readListener = (e) => {
111 e.preventDefault();
112 addReadReply(id);
113 top.classList.remove("highlight");
114 newCmtMarker.remove();
115 markReadBtn.remove();
116 jumpLinks.querySelector(`a[href="#cmt${id}"]`).parentNode.remove();
117 if (expandListener) {
118 removeEventListener('click', expandListener);
119 }
120 };
121
122 markReadBtn.addEventListener('click', readListener);
123
124 if (top.querySelector('.partial')) {
125 top.querySelector('.comment .inner').append(markReadBtn);
126 expandListener = top.querySelector('a[onclick]').addEventListener('click', (_e) => {
127 newCmtMarker.remove();
128 markReadBtn.remove();
129 const waitForExpansion = setTimeout(() => {
130 if (top.querySelector('.comment-info h4')) {
131 top.querySelector('.comment-info h4').append(newCmtMarker);
132 top.querySelector('.comment-info h4').append(markReadBtn);
133 markReadBtn.addEventListener('click', readListener);
134 clearInterval(waitForExpansion);
135 }
136 }, 1000);
137 });
138 } else {
139 top.querySelector('.comment-info h4').append(markReadBtn);
140 }
141 };
142
143 const main = () => {
144 // create styles for stuff we're adding to the UI
145 const newStyles = document.createElement('style');
146 newStyles.setAttribute('type', 'text/css');
147 newStyles.textContent = `
148 .highlight .partial > .comment,
149 .highlight .visible .comment-info {
150 background-color: ${highlightColor} !important;
151 }
152
153 button.markRead {
154 padding: 2px;
155 margin-bottom: 0;
156 font-size: .8em;
157 }
158
159 .comment .inner > button.markRead {
160 margin-left: 5px;
161 }
162
163 #jump-links {
164 position: fixed;
165 bottom: 10px;
166 left: 10px;
167 padding: 5px;
168 border: 1px gray solid;
169 background-color: #FFFFFF66;
170 max-height: 150px;
171 max-width: 200px;
172 overflow: auto;
173 opacity: .5;
174 }
175
176 #jump-links summary {
177 cursor: pointer;
178 list-style: none;
179 }
180
181 #jump-links > a {
182 text-decoration: none;
183 color: inherit;
184 }
185
186 #jump-links ol {
187 font-size: .8em;
188 margin-bottom: 0;
189 }
190
191 #jump-links:hover,
192 #jump-links:focus-within {
193 opacity: 1;
194 }
195 `;
196 document.head.append(newStyles);
197
198 readReplies = localStorage.getItem("readComments") ? JSON.parse(localStorage.getItem("readComments")) : [];
199
200 let prevDepth = 1;
201 let topOfThread = null;
202 let prevComment = null;
203
204 // loop over all comments
205 document.querySelectorAll('.comment-thread').forEach((cmt) => {
206 // get the depth from the comment classes
207 const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", ""));
208
209 // if we're not currently in a thread, check if this is the top of a thread (either belongs to us or is a toplevel)
210 if (!topOfThread || depth === 1) {
211 if (!cmt.querySelector('.comment-wrapper').classList.contains(`poster-${username}`)) {
212 prevComment = null;
213 return;
214 }
215 topOfThread = cmt;
216 prevComment = cmt;
217 }
218
219 // if it's just a reply in the same thread but not the end keep going
220 if (depth >= prevDepth) {
221 prevComment = cmt;
222 prevDepth = depth;
223 return;
224 }
225
226 // if the depth is lower than it was in the last comment, the thread has ended
227 if (depth < prevDepth) {
228 const id = parseInt(topOfThread.querySelector('.comment').id.replace("comment-cmt", ""));
229
230 console.log(`depth: ${depth}; prevDepth: ${prevDepth}`);
231 console.log('comment');
232 console.log(cmt);
233 console.log('top of thread');
234 console.log(topOfThread);
235 console.log('prev comment');
236 console.log(prevComment);
237 // if this comment isn't ours and it's not in our ignored threads, highlight it
238 if (prevComment && !prevComment.querySelector('.comment-wrapper').classList.contains(`poster-${username}`) && readReplies.indexOf(id) === -1 && cmt !== topOfThread) {
239
240 highlightCmt(cmt, topOfThread);
241 linkList.push(id);
242 }
243
244 // regardless, unset the thread variables
245 topOfThread = null;
246 prevComment = null;
247 prevDepth = 1;
248 return;
249 }
250
251 // set variables for next loop
252 prevComment = cmt;
253 prevDepth = depth;
254 });
255
256 // add comments to the floating bottom left menu
257 linkList.forEach((id) => {
258 const newLi = document.createElement('li');
259 const newA = document.createElement('a');
260 newA.href = `#cmt${id}`;
261 newA.textContent = `#${id}`;
262 newLi.append(newA);
263 jumpLinksOl.append(newLi);
264 });
265 };
266
267
268 main();
269 }
270})();