// ==UserScript== // @name Reddit - Highlight New Comments // @version 1.1.1 // @description Highlights new comments since your last visit // @author Journey Over // @license MIT // @match *://*.reddit.com/r/*/comments/*/* // @require https://cdn.jsdelivr.net/gh/bgrins/TinyColor@13851a7f4950040d9ad8557c3a92d9f4d8d02843/tinycolor.min.js // @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@0171b6b6f24caea737beafbc2a8dacd220b729d8/libs/utils/utils.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com // @homepageURL https://github.com/StylusThemes/Userscripts // @downloadURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/reddit-highlight-new-comments.user.js // @updateURL https://github.com/StylusThemes/Userscripts/raw/main/userscripts/reddit-highlight-new-comments.user.js // ==/UserScript== 'use strict'; /* global tinycolor */ const logger = Logger('Reddit Highlight New Comments', { debug: false }); const HNC = { init() { if (!document.getElementById('siteTable')) return; const threadElement = document.querySelector('#siteTable .thing.link'); if (!threadElement) return; let thread = threadElement.getAttribute('data-fullname'); if (!thread) { const match = threadElement.className.match(/id-(t3_[^ ]+)/); if (!match) return; thread = match[1]; } const now = Date.now(); this.config = this.cfg.load(); this.clear_history(); if (!this.config.history[thread]) { this.config.history[thread] = []; } this.config.history[thread].unshift(now); if (!document.getElementById('noresults')) { if (this.config.history[thread].length > 1) { this.highlight(this.config.history[thread][1]); this.setupResStyles(); this.ui.create_comment_highlighter(this.config.history[thread]); this.ui.create_config_dialog(); GM_addStyle(this.data.config_style); } } this.cfg.save(); }, _getLoggedInUsername() { if (!document.body.classList.contains('loggedin')) return null; const userElement = document.getElementsByClassName('user')[0]; return userElement?.firstElementChild?.textContent || null; }, _applyHighlightStyle(comment, tagline, time, since) { const elements = { 'comment': comment, 'text': comment.getElementsByClassName('usertext-body')[0]?.firstElementChild, 'time': comment.getElementsByClassName('live-timestamp')[0] || tagline.getElementsByTagName('time')[0], }; const targetElement = elements[this.config.apply_on]; if (!targetElement) return; targetElement.setAttribute('style', this.generate_comment_style(time, since)); if (this.config.apply_on === 'comment') { const color = this.get_color(Date.now() - time, Date.now() - since); comment.style.setProperty('--hnc-color', color); comment.style.setProperty('--hnc-color-selected', tinycolor(color).darken(2).toHslString()); } }, highlight(since) { const comments = document.getElementsByClassName('comment'); const username = this._getLoggedInUsername(); for (const comment of comments) { if (comment.classList.contains('deleted') || comment.classList.contains('spam')) continue; const authorElement = comment.getElementsByClassName('author')[0]; if (!authorElement) continue; if (username && username === authorElement.textContent) continue; const tagline = comment.getElementsByClassName('tagline')[0]; if (!tagline) continue; const times = tagline.getElementsByTagName('time'); if (!times.length) continue; const timeIndex = this.config.prefer_edited_time ? times.length - 1 : 0; const time = Date.parse(times[timeIndex].getAttribute('datetime')); if (time > since) { comment.classList.add('hnc_new'); this._applyHighlightStyle(comment, tagline, time, since); } } }, reset_highlighting() { const comments = document.getElementsByClassName('hnc_new'); for (let index = comments.length - 1; index >= 0; index--) { const comment = comments[index]; comment.classList.remove('hnc_new'); const elements = { 'comment': comment, 'text': comment.getElementsByClassName('usertext-body')[0]?.firstElementChild, 'time': comment.getElementsByTagName('time')[0], }; for (const key in elements) { if (elements[key]) { elements[key].removeAttribute('style'); } } } }, clear_history() { const now = Date.now(); const expiration = this.config.history_expiration * 24 * 60 * 60 * 1000; for (const thread in this.config.history) { const visits = this.config.history[thread]; for (let index = 0; index < visits.length; index++) { if (now - visits[index] > expiration) { this.config.history[thread].splice(index); if (!this.config.history[thread].length) { delete this.config.history[thread]; } } } } }, generate_comment_style(comment_time, since) { let style = this.config.comment_style; style = style.replace(/\s+/g, ' '); style = style.replace(/%color/g, this.get_color(Date.now() - comment_time, Date.now() - since)); return style; }, get_color(comment_age, highlighting_since) { if (!this.config.use_color_gradient) return this.config.color_newer; if (comment_age > highlighting_since - 1) return this.config.color_older; const time_diff = 1 - comment_age / highlighting_since; const color_newer = tinycolor(this.config.color_newer).toHsl(); const color_older = tinycolor(this.config.color_older).toHsl(); const a_newer = color_newer.a !== undefined ? color_newer.a : 1; const a_older = color_older.a !== undefined ? color_older.a : 1; const color_final = tinycolor({ h: color_older.h + (color_newer.h - color_older.h) * time_diff, s: color_older.s + (color_newer.s - color_older.s) * time_diff, l: color_older.l + (color_newer.l - color_older.l) * time_diff, a: a_older + (a_newer - a_older) * time_diff }); return color_final.toHslString(); }, setupResStyles() { const existing = document.getElementById('hnc-res-styles'); if (existing) existing.remove(); if (this.config.apply_on !== 'comment') return; const style = document.createElement('style'); style.id = 'hnc-res-styles'; style.textContent = [ '.res-nightmode .hnc_new .entry.res-selected,', '.res-nightmode .hnc_new .entry.res-selected .md-container {', ' background-color: var(--hnc-color-selected) !important;', '}', '.res .commentarea .hnc_new .RES-keyNav-activeElement,', '.res .commentarea .hnc_new .RES-keyNav-activeElement .md,', '.res .commentarea .hnc_new .RES-keyNav-activeElement.entry .noncollapsed {', ' background-color: var(--hnc-color-selected) !important;', '}', ].join('\n'); document.head.appendChild(style); }, }; HNC.ui = { create_comment_highlighter(visits) { const highlighter = document.createElement('div'); highlighter.innerHTML = HNC.data.comment_highlighter; highlighter.classList.add('rounded', 'comment-visits-box', 'hnc-toolbar'); if (HNC.config.dark_mode) { highlighter.classList.add('hnc-dark-mode'); } else { highlighter.classList.add('hnc-light-mode'); } const commentarea = document.getElementsByClassName('commentarea')[0]; if (!commentarea) return; const sitetable = commentarea.getElementsByClassName('sitetable')[0]; if (!sitetable || !sitetable.firstChild) return; const comment_margin = window.getComputedStyle(sitetable.firstChild).getPropertyValue('margin-left'); const existing_highlighter = document.getElementsByClassName('comment-visits-box')[0]; if (existing_highlighter && !existing_highlighter.classList.contains('hnc-toolbar')) { existing_highlighter.parentNode.removeChild(existing_highlighter); } highlighter.style.setProperty('margin-left', comment_margin); sitetable.before(highlighter); const select = document.getElementById('comment-visits'); const seenLabels = new Set(); for (const visit of visits) { const label = time_ago(visit); if (seenLabels.has(label)) continue; seenLabels.add(label); const option = document.createElement('option'); option.textContent = label; option.value = visit; select.appendChild(option); } if (visits.length > 1 && select.children[3]) { select.children[3].setAttribute('selected', ''); } select.addEventListener('change', this.update_highlighting.bind(this)); const custom = document.getElementById('hnc_custom_visit'); custom.style.setProperty('width', `${select.getBoundingClientRect().width}px`); custom.addEventListener('keydown', this.custom_visit_key_monitor.bind(this)); custom.addEventListener('blur', this.set_custom_visit.bind(this)); this.custom_pos = 0; const config_button = document.getElementById('hnc_config_icon'); config_button.innerHTML = HNC.data.gear_icon; config_button.addEventListener('click', this.show_config_dialog.bind(this)); }, update_highlighting(event) { if (event.target.value === '') { HNC.reset_highlighting(); } else if (event.target.value === 'custom') { document.getElementById('comment-visits').style.setProperty('display', 'none'); const custom = document.getElementById('hnc_custom_visit'); custom.style.removeProperty('display'); custom.focus(); custom.setSelectionRange(0, 2); } else { HNC.reset_highlighting(); HNC.highlight(parseInt(event.target.value, 10)); } }, custom_visit_key_monitor(event) { if (event.altKey || event.ctrlKey || (event.shiftKey && event.key !== 'Tab')) return; if (event.key === 'Tab') { const match = event.target.value.match(/^(\d+?:)\d+?$/); if (match) { event.shiftKey ? this.custom_pos-- : this.custom_pos++; if (this.custom_pos % 2 === 0) { event.target.setSelectionRange(0, match[1].length - 1); } else { event.target.setSelectionRange(match[1].length, match[0].length); } event.preventDefault(); event.stopPropagation(); } } else if (event.key === 'Enter') { event.target.blur(); event.preventDefault(); event.stopPropagation(); } }, set_custom_visit(event) { const select = document.getElementById('comment-visits'); const match = event.target.value.match(/^(\d+?):(\d+?)$/); if (match) { const option = document.createElement('option'); const hours = parseInt(match[1], 10); const minutes = parseInt(match[2], 10); const visit = Date.now() - (hours * 60 + minutes) * 60 * 1000; option.value = visit; option.textContent = time_ago(visit); select.add(option, 2); select.selectedIndex = 2; } else { select.selectedIndex = 0; } select.dispatchEvent(new Event('change')); event.target.value = '00:00'; event.target.style.setProperty('display', 'none'); select.style.removeProperty('display'); }, create_config_dialog() { const wrapper = document.createElement('div'); document.body.appendChild(wrapper); wrapper.id = 'hnc_dialog_wrapper'; wrapper.innerHTML = HNC.data.config_dialog; if (HNC.config.dark_mode) wrapper.classList.add('hnc-dark-mode'); const comment_preview = document.getElementById('hnc_comment_preview'); const first_comment_source = document.getElementsByClassName('comment')[0]; if (first_comment_source) { const first_comment = first_comment_source.cloneNode(true); const childContainer = first_comment.getElementsByClassName('child')[0]; if (childContainer) first_comment.removeChild(childContainer); first_comment.style.setProperty('margin-left', '0'); comment_preview.appendChild(first_comment); } wrapper.style.setProperty('display', 'none'); wrapper.addEventListener('click', this.hide_config_dialog.bind(this)); this.load_config_values(); this.add_listeners(); }, show_config_dialog() { document.getElementById('hnc_dialog_wrapper').style.removeProperty('display'); }, hide_config_dialog(event) { if (event.target.id !== 'hnc_dialog_wrapper' && event.target.id !== 'hnc_close_button') return; document.getElementById('hnc_dialog_wrapper').style.setProperty('display', 'none'); HNC.reset_highlighting(); const selectValue = document.getElementById('comment-visits').value; if (selectValue) { HNC.highlight(parseInt(selectValue, 10)); } if (event.target.id === 'hnc_close_button') { HNC.cfg.save(); } }, load_config_values() { const dialog_settings = document.getElementsByClassName('hnc_setting'); for (const element of dialog_settings) { const name = element.id.slice(4); if (element.tagName === 'INPUT' && element.type === 'checkbox') { element.checked = HNC.config[name]; if (element.dataset.disable) { document.getElementById(element.dataset.disable).disabled = !element.checked; } } else { element.value = HNC.config[name] || ''; } } this.update_preview(); }, add_listeners() { const dialog_settings = document.getElementsByClassName('hnc_setting'); for (const element of dialog_settings) { element.addEventListener('change', this.setting_change.bind(this)); if (element.tagName === 'INPUT' && element.type === 'text' || element.tagName === 'TEXTAREA') { element.addEventListener('input', this.setting_change.bind(this)); } } document.getElementById('hnc_clear_history_button').addEventListener('click', this.clear_all_history.bind(this)); document.getElementById('hnc_reset_button').addEventListener('click', this.reset_config.bind(this)); document.getElementById('hnc_close_button').addEventListener('click', this.hide_config_dialog.bind(this)); }, setting_change(event) { const name = event.target.id.slice(4); if (event.target.tagName === 'INPUT' && event.target.type === 'text' && !event.target.validity.valid) { if (event.type === 'change') { event.target.value = HNC.config[name]; } return; } if (event.target.tagName === 'INPUT' && event.target.type === 'checkbox') { HNC.config[name] = event.target.checked; if (name === 'dark_mode') { document.getElementById('hnc_dialog_wrapper').classList.toggle('hnc-dark-mode', event.target.checked); const toolbar = document.querySelector('.hnc-toolbar'); if (toolbar) { toolbar.classList.toggle('hnc-dark-mode', event.target.checked); toolbar.classList.toggle('hnc-light-mode', !event.target.checked); } } if (event.target.dataset.disable) { document.getElementById(event.target.dataset.disable).disabled = !event.target.checked; } } else { HNC.config[name] = event.target.value; } if (name === 'apply_on') HNC.setupResStyles(); this.update_preview(); }, reset_config() { const history = HNC.config.history; HNC.config = HNC.cfg.default(); HNC.config.history = history; document.getElementById('hnc_dialog_wrapper').classList.toggle('hnc-dark-mode', HNC.config.dark_mode); const toolbar = document.querySelector('.hnc-toolbar'); if (toolbar) { toolbar.classList.toggle('hnc-dark-mode', HNC.config.dark_mode); toolbar.classList.toggle('hnc-light-mode', !HNC.config.dark_mode); } HNC.setupResStyles(); this.load_config_values(); }, clear_all_history() { HNC.config.history = {}; alert('History cleared.'); }, update_preview() { const previewContainer = document.getElementById('hnc_comment_preview'); if (!previewContainer.firstElementChild) return; const preview = previewContainer.firstElementChild; const elements = { 'comment': preview, 'text': preview.getElementsByClassName('usertext-body')[0]?.firstElementChild, 'time': preview.getElementsByClassName('live-timestamp')[0] || preview.getElementsByTagName('time')[0], }; for (const key in elements) { if (elements[key]) elements[key].removeAttribute('style'); } if (elements.time) { const comment_age = Date.parse(elements.time.getAttribute('dateTime')); const double_comment_age = comment_age - (Date.now() - comment_age) * 2; if (elements[HNC.config.apply_on]) { elements[HNC.config.apply_on].setAttribute('style', HNC.generate_comment_style(comment_age, double_comment_age)); } } }, }; HNC.data = { gear_icon: ``, comment_highlighter: `