Dreamwidth New Comment Highlighter

Highlight new comments in a Dreamwidth comment thread

// ==UserScript==
// @name         Dreamwidth New Comment Highlighter
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Highlight new comments in a Dreamwidth comment thread
// @author       conroicht
// @match        *://*.dreamwidth.org/*
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @license      MIT
// ==/UserScript==

/* jshint esversion:8 */

(function() {
    'use strict';

    const DEFAULT_HIGHLIGHT_BG = 'green';
    const DEFAULT_HIGHLIGHT_TEXT = 'white';

    const BG_KEY_NAME = 'hightlight_bg';
    const TEXT_KEY_NAME = 'hightlight_text';

    const BASE_KEY_NAME = 'commentHighlighter';
    const STAMP_LIFE_IN_DAYS = 3;

    const SEARCH_KEYS = ['page', 'thread', 'view'];


    GM_registerMenuCommand('Set background color', async () => {
        const colour = prompt('Set the highlight background color');
        if (colour) {
            await GM.setValue(BG_KEY_NAME, colour);
        }
    });

    GM_registerMenuCommand('Set text color', async () => {
        const colour = prompt('Set highlight text color');
        if (colour) {
            await GM.setValue(TEXT_KEY_NAME, colour);
        }
    });

    async function setColorDefaults() {
        const background = await GM.getValue(BG_KEY_NAME);
        const text = await GM.getValue(TEXT_KEY_NAME);

        if (!background) {
            await GM.setValue(BG_KEY_NAME, DEFAULT_HIGHLIGHT_BG);
        }

        if (!text) {
            await GM.setValue(TEXT_KEY_NAME, DEFAULT_HIGHLIGHT_TEXT);
        }
    }

    async function clearOldStamps() {
        const msInOneDay = 86400000;
        const oldestAllowableDate = new Date() - (STAMP_LIFE_IN_DAYS * msInOneDay);

        const currentStoredKeys = await GM.listValues();
        const stampKeys = currentStoredKeys.filter(key => key.includes(BASE_KEY_NAME));

        return Promise.all(stampKeys.map(async (keyName) => {
            const timestamp = await GM.getValue(keyName);
            const date = new Date(timestamp);
            if (date < oldestAllowableDate) {
                await GM.deleteValue(keyName);
            }
        }));
    }

    function getMilitaryTime(time, period) {
        const [hours, minutes] = time.split(':');
        const hoursAsInteger = Number.parseInt(hours, 10);
        if (period === 'pm' && hoursAsInteger !== 12) return `${hoursAsInteger + 12}:${minutes}`;
        if (period === 'am' && hoursAsInteger == 12) return `00:${minutes}`;
        return time;
    }

    function parseDateFromTimestamp(stamp) {
        const [date, time, period] = stamp.split(' ');
        return new Date(`${date}T${getMilitaryTime(time, period)}Z`);
    }

    function getTimestamps(rootNode = document) {
        return Array.from(rootNode.querySelectorAll('.comment .datetime'))
            .map((element) => ({ element, date: parseDateFromTimestamp(element.lastElementChild.textContent) }));
    }

    function getUrlSearchAsObject(search) {
        return search.replace('?', '').split('&').reduce((searches, searchString) => {
            const [key, value] = searchString.split('=');
            searches[key] = value;
            return searches;
        }, Object.create(null));
    }

    function getThreadKey() {
        const { pathname, search } = window.location;
        const searchKeyPairs = getUrlSearchAsObject(search === '?page=1' ? '' : search);

        const searchStringForThreadKey = SEARCH_KEYS.reduce((keyString, key) => {
            if (searchKeyPairs[key]) {
                return `${keyString}&${key}=${searchKeyPairs[key]}`;
            }
            return keyString;
        }, '');

        return `${BASE_KEY_NAME}${pathname}${searchStringForThreadKey}`;
    }

    function saveLastRefresh() {
        const now = new Date();
        return GM.setValue(getThreadKey(), now.toISOString());
    }

    async function getLastRefresh() {
        const lastRefreshTime = await GM.getValue(getThreadKey());
        return lastRefreshTime ? new Date(lastRefreshTime) : null;
    }

    async function highlightComments(lastRefresh, rootNode = document) {
        const color = await GM.getValue(TEXT_KEY_NAME);
        const background = await GM.getValue(BG_KEY_NAME);
        getTimestamps(rootNode).forEach(({ element, date }) => {
            if (date > lastRefresh) {
                element.setAttribute('style', `color:${color};background:${background};`);
                element.textContent += ' NEW';
            }
        });
    }

    (async function run() {
        await setColorDefaults();
        const lastRefresh = await getLastRefresh();

        if (lastRefresh) {
            await highlightComments(lastRefresh);
            const observer = new MutationObserver((records) => {
                records.forEach((record) => {
                    record.addedNodes.forEach(async (node) => {
                        if (node.querySelector) {
                            await highlightComments(lastRefresh, node);
                        }
                    });
                });
            });
            observer.observe(document.querySelector('#comments'), { subtree: true, childList: true });
        }
        await clearOldStamps();
        await saveLastRefresh();
    })();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址