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 343f7783e010b0baa4e6ce20f0f919b699b2dcee

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