// ==UserScript== // @name Highlight New Replies // @namespace http://veryroundbird.house // @version 2026-06-03 // @description highlights your threads where you're not the most recent reply. // @author Carly Smallbird // @match https://*.dreamwidth.org/* // @icon https://www.google.com/s2/favicons?sz=64&domain=dreamwidth.org // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @grant GM_getValue // @grant GM_setValue // @grant GM.getValue // @grant GM.setValue // ==/UserScript== let gmc = new GM_config( { 'id': 'veryroundbirdhouse_newreplies', 'title': 'Settings', 'fields': { 'newReplyText': { 'label': 'New Reply Text', 'type': 'text', 'default': '✨ NEW REPLIES ✨' }, 'markDoneText': { 'label': 'Mark Thread Done Text', 'type': 'text', 'default': '✅ Mark Thread Done' }, 'highlightColor': { 'label': 'Highlight Color', 'type': 'text', 'default': 'yellow' } }, 'init': () => { gmc.css.basic = ` label { display: block; } `; } }); (function() { 'use strict'; let readReplies = []; let linkList = []; const username = document.querySelector('#account-links-text .ljuser').getAttribute('lj:user'); const newReplyText = "✨ NEW REPLIES ✨"; const markDoneText = "✅ Mark Thread Done"; const highlightColor = "yellow"; const jumpLinks = document.createElement('div'); const jumpDetails = document.createElement('details'); const jumpSummary = document.createElement('summary'); const jumpLinksOl = document.createElement('ol'); const settingsLink = document.createElement('a'); jumpSummary.textContent = "🧵 New Comments"; jumpDetails.append(jumpSummary); jumpDetails.append(jumpLinksOl); jumpLinks.append(jumpDetails); settingsLink.textContent = "⚙️ Settings"; settingsLink.href = "#"; settingsLink.addEventListener('click', (e) => { e.preventDefault(); gmc.open(); }); jumpLinks.append(settingsLink); jumpLinks.id = 'jump-links'; document.body.append(jumpLinks); const addReadReply = (id) => { if (readReplies.indexOf(parseInt(id)) === -1) { readReplies.push(parseInt(id)); localStorage.setItem("readComments", JSON.stringify(readReplies)); } }; const highlightCmt = (cmt, top) => { const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", "")); const id = parseInt(top.querySelector('.comment').id.replace("comment-cmt", "")); const info = top.querySelector('.comment-info'); const markReadBtn = document.createElement('button'); const newCmtMarker = document.createElement('span'); let expandListener = null; markReadBtn.setAttribute('type', 'button'); markReadBtn.classList.add("markRead"); markReadBtn.textContent = markDoneText; newCmtMarker.textContent = newReplyText; newCmtMarker.classList.add('newComment'); top.classList.add('highlight'); const readListener = (e) => { e.preventDefault(); addReadReply(id); top.classList.remove("highlight"); newCmtMarker.remove(); markReadBtn.remove(); jumpLinks.querySelector(`a[href="#cmt${id}"]`).parentNode.remove(); if (expandListener) { removeEventListener('click', expandListener); } }; markReadBtn.addEventListener('click', readListener); if (top.querySelector('.partial')) { top.querySelector('.comment .inner').append(markReadBtn); expandListener = top.querySelector('a[onclick]').addEventListener('click', (_e) => { newCmtMarker.remove(); markReadBtn.remove(); const waitForExpansion = setTimeout(() => { if (top.querySelector('.comment-info h4')) { top.querySelector('.comment-info h4').append(newCmtMarker); top.querySelector('.comment-info h4').append(markReadBtn); markReadBtn.addEventListener('click', readListener); clearInterval(waitForExpansion); } }, 1000); }); } else { top.querySelector('.comment-info h4').append(markReadBtn); } }; const main = () => { const newStyles = document.createElement('style'); newStyles.setAttribute('type', 'text/css'); newStyles.textContent = ` .highlight .partial > .comment, .highlight .visible .comment-info { background-color: ${highlightColor} !important; } button.markRead { padding: 2px; margin-bottom: 0; font-size: .8em; } .comment .inner > button.markRead { margin-left: 5px; } #jump-links { position: fixed; bottom: 10px; left: 10px; padding: 5px; border: 1px gray solid; background-color: #FFFFFF66; max-height: 150px; max-width: 200px; overflow: auto; opacity: .5; } #jump-links summary { cursor: pointer; list-style: none; } #jump-links > a { text-decoration: none; color: inherit; } #jump-links ol { font-size: .8em; margin-bottom: 0; } #jump-links:hover, #jump-links:focus-within { opacity: 1; } `; document.head.append(newStyles); readReplies = localStorage.getItem("readComments") ? JSON.parse(localStorage.getItem("readComments")) : []; let prevDepth = 1; let topOfThread = null; let prevComment = null; document.querySelectorAll('.comment-thread').forEach((cmt) => { const depth = parseInt(Array.from(cmt.classList).filter(cl => cl.match(/comment\-depth\-[0-9]+/))[0].replace("comment-depth-", "")); if (!topOfThread || depth === 1) { if (!cmt.querySelector('.comment-wrapper').classList.contains(`poster-${username}`)) { prevComment = null; return; } topOfThread = cmt; prevComment = cmt; return; } if (depth >= prevDepth) { prevComment = cmt; prevDepth = depth; return; } if (depth < prevDepth) { const id = parseInt(topOfThread.querySelector('.comment').id.replace("comment-cmt", "")); if (prevComment && !prevComment.querySelector('.comment-wrapper').classList.contains(`poster-${username}`) && readReplies.indexOf(id) === -1) { highlightCmt(cmt, topOfThread); linkList.push(id); } topOfThread = null; prevComment = null; prevDepth = 1; return; } prevComment = cmt; prevDepth = depth; }); linkList.forEach((id) => { const newLi = document.createElement('li'); const newA = document.createElement('a'); newA.href = `#cmt${id}`; newA.textContent = `#${id}`; newLi.append(newA); jumpLinksOl.append(newLi); }); }; main(); })();