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 9c4c501c00b44b83bcad5efe532c4e4dbbd043e1

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
16let gmc = new GM_config(
17{
18 'id': 'veryroundbirdhouse_newreplies',
19 'title': 'Settings',
20 'fields':
21 {
22 'newReplyText':
23 {
24 'label': 'New Reply Text',
25 'type': 'text',
26 'default': '✨ NEW REPLIES ✨'
27 },
28 'markDoneText':
29 {
30 'label': 'Mark Thread Done Text',
31 'type': 'text',
32 'default': '✅ Mark Thread Done'
33 },
34 'highlightColor':
35 {
36 'label': 'Highlight Color',
37 'type': 'text',
38 'default': 'yellow'
39 }
40 },
41 'init': () => {
42 gmc.css.basic = `
43 label {
44 display: block;
45 }
46 `;
47 }
48});
49
50(function() {
51 'use strict';
52 let readReplies = [];
53 let linkList = [];
54 const username = document.querySelector('#account-links-text .ljuser').getAttribute('lj:user');
55 const newReplyText = "✨ NEW REPLIES ✨";
56 const markDoneText = "✅ Mark Thread Done";
57 const highlightColor = "yellow";
58 const jumpLinks = document.createElement('div');
59 const jumpDetails = document.createElement('details');
60 const jumpSummary = document.createElement('summary');
61 const jumpLinksOl = document.createElement('ol');
62 const settingsLink = document.createElement('a');
63 jumpSummary.textContent = "🧵 New Comments";
64 jumpDetails.append(jumpSummary);
65 jumpDetails.append(jumpLinksOl);
66 jumpLinks.append(jumpDetails);
67 settingsLink.textContent = "⚙️ Settings";
68 settingsLink.href = "#";
69 settingsLink.addEventListener('click', (e) => {
70 e.preventDefault();
71 gmc.open();
72 });
73 jumpLinks.append(settingsLink);
74 jumpLinks.id = 'jump-links';
75 document.body.append(jumpLinks);
76
77 const addReadReply = (id) => {
78 if (readReplies.indexOf(parseInt(id)) === -1) {
79 readReplies.push(parseInt(id));
80 localStorage.setItem("readComments", JSON.stringify(readReplies));
81 }
82 };
83
84 const highlightCmt = (cmt, top) => {
85 const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", ""));
86 const id = parseInt(top.querySelector('.comment').id.replace("comment-cmt", ""));
87 const info = top.querySelector('.comment-info');
88 const markReadBtn = document.createElement('button');
89 const newCmtMarker = document.createElement('span');
90 let expandListener = null;
91 markReadBtn.setAttribute('type', 'button');
92 markReadBtn.classList.add("markRead");
93 markReadBtn.textContent = markDoneText;
94 newCmtMarker.textContent = newReplyText;
95 newCmtMarker.classList.add('newComment');
96 top.classList.add('highlight');
97
98 const readListener = (e) => {
99 e.preventDefault();
100 addReadReply(id);
101 top.classList.remove("highlight");
102 newCmtMarker.remove();
103 markReadBtn.remove();
104 jumpLinks.querySelector(`a[href="#cmt${id}"]`).parentNode.remove();
105 if (expandListener) {
106 removeEventListener('click', expandListener);
107 }
108 };
109
110 markReadBtn.addEventListener('click', readListener);
111
112 if (top.querySelector('.partial')) {
113 top.querySelector('.comment .inner').append(markReadBtn);
114 expandListener = top.querySelector('a[onclick]').addEventListener('click', (_e) => {
115 newCmtMarker.remove();
116 markReadBtn.remove();
117 const waitForExpansion = setTimeout(() => {
118 if (top.querySelector('.comment-info h4')) {
119 top.querySelector('.comment-info h4').append(newCmtMarker);
120 top.querySelector('.comment-info h4').append(markReadBtn);
121 markReadBtn.addEventListener('click', readListener);
122 clearInterval(waitForExpansion);
123 }
124 }, 1000);
125 });
126 } else {
127 top.querySelector('.comment-info h4').append(markReadBtn);
128 }
129 };
130
131 const main = () => {
132 const newStyles = document.createElement('style');
133 newStyles.setAttribute('type', 'text/css');
134 newStyles.textContent = `
135 .highlight .partial > .comment,
136 .highlight .visible .comment-info {
137 background-color: ${highlightColor} !important;
138 }
139
140 button.markRead {
141 padding: 2px;
142 margin-bottom: 0;
143 font-size: .8em;
144 }
145
146 .comment .inner > button.markRead {
147 margin-left: 5px;
148 }
149
150 #jump-links {
151 position: fixed;
152 bottom: 10px;
153 left: 10px;
154 padding: 5px;
155 border: 1px gray solid;
156 background-color: #FFFFFF66;
157 max-height: 150px;
158 max-width: 200px;
159 overflow: auto;
160 opacity: .5;
161 }
162
163 #jump-links summary {
164 cursor: pointer;
165 list-style: none;
166 }
167
168 #jump-links > a {
169 text-decoration: none;
170 color: inherit;
171 }
172
173 #jump-links ol {
174 font-size: .8em;
175 margin-bottom: 0;
176 }
177
178 #jump-links:hover,
179 #jump-links:focus-within {
180 opacity: 1;
181 }
182 `;
183 document.head.append(newStyles);
184
185 readReplies = localStorage.getItem("readComments") ? JSON.parse(localStorage.getItem("readComments")) : [];
186 let prevDepth = 1;
187 let topOfThread = null;
188 let prevComment = null;
189 document.querySelectorAll('.comment-thread').forEach((cmt) => {
190 const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", ""));
191 if (!topOfThread || depth === 1) {
192 if (!cmt.querySelector('.comment-wrapper').classList.contains(`poster-${username}`)) {
193 prevComment = null;
194 return;
195 }
196 topOfThread = cmt;
197 prevComment = cmt;
198 return;
199 }
200 if (depth >= prevDepth) {
201 prevComment = cmt;
202 prevDepth = depth;
203 return;
204 }
205 if (depth < prevDepth) {
206 const id = parseInt(topOfThread.querySelector('.comment').id.replace("comment-cmt", ""));
207 if (prevComment && !prevComment.querySelector('.comment-wrapper').classList.contains(`poster-${username}`) && readReplies.indexOf(id) === -1) {
208 highlightCmt(cmt, topOfThread);
209 linkList.push(id);
210 }
211 topOfThread = null;
212 prevComment = null;
213 prevDepth = 1;
214 return;
215 }
216 prevComment = cmt;
217 prevDepth = depth;
218 });
219
220 linkList.forEach((id) => {
221 const newLi = document.createElement('li');
222 const newA = document.createElement('a');
223 newA.href = `#cmt${id}`;
224 newA.textContent = `#${id}`;
225 newLi.append(newA);
226 jumpLinksOl.append(newLi);
227 });
228 };
229
230 main();
231})();