AO3: Badge for Unread Inbox Messages

puts a little notification badge in the menu for unread messages in your AO3 inbox

// ==UserScript==
// @name         AO3: Badge for Unread Inbox Messages
// @namespace    https://greasyfork.org/en/users/906106-escctrl
// @version      2.0
// @description  puts a little notification badge in the menu for unread messages in your AO3 inbox
// @author       escctrl
// @match        https://*.archiveofourown.org/*
// @license      MIT
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
// @grant        none
// ==/UserScript==
 
/****************** CONFIGURATION ******************/
 
// how often the script will check for unread messages (in hours)
const REFRESH_INTERVAL = 12;
 
// if the badge should show as an icon (true), or as text (false)
const BADGE_ICON = true;
 
// pick a background color for the badge to stand out more, or leave empty quotes ""
const HIGHLIGHT_COLOR = "gold";
 
// if the inbox link in the sidebar should automatically filter to unread messages only
const FILTER_INBOX = false;
 
 
// ****************** NOTE ON LOCAL STORAGE ******************
// For compatibility between userscript managers, this script uses local storage, which is visible from the Developer console.
// If you ever uninstall the script, unfortunately its data can't be automatically deleted.
// If you want to remove the data it sets, (1) visit archiveofourown.org, (2) go into the Developer console,
// (3) look for the Local Storage (4) and delete the entries for "unread_inbox_count" and "unread_inbox_date".
// The script also removes its data if you ever visit AO3 while logged out.
 
 
(function($) {
    'use strict';
 
    // first question: is the user logged in? if not, don't bother with any of this
    const linkDash = $("#greeting p.icon a").attr('href') || "";
    if (linkDash === "") {
        localStorage.removeItem('unread_inbox_count');
        localStorage.removeItem('unread_inbox_date');
        return;
    }
 
    var highlight_css = (HIGHLIGHT_COLOR !== "") ? `#greeting #inboxbadge { background-color: ${HIGHLIGHT_COLOR}; border-radius: .25em; }` : "";
 
    $("head").append(`<style type="text/css"> a#inboxbadge .iconify { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
        a#inboxbadge { display: block; padding: .25em .75em !important; text-align: center; float: left; margin: 0 1em; line-height: 1.286; height: 1.286em; }
        p.icon a { float: right; } ${highlight_css}</style>`);
 
    // build a new inbox link (filtered to unread)
    const linkInbox = linkDash + "/inbox?filters[read]=false&filters[replied_to]=all&filters[date]=desc&commit=Filter";
 
    // the fun begins: on a page where we're seeing the unread msgs, we simply set the value
    var page_url = window.location.pathname;
    if (page_url.includes(linkDash)) {
 
        // grab unread msgs # from the sidebar
        var badge = (page_url.includes("/inbox")) ? $("div#dashboard li span.current").html() : $("div#dashboard a[href$='inbox']").html();
        badge = badge.match(/\d+/);
 
        // store the currently seen value with the current date, on every page visit, no questions asked
        localStorage.setItem('unread_inbox_count', badge);
        localStorage.setItem('unread_inbox_date', new Date());
 
        // change sidebar inbox link as well to filtered
        if (FILTER_INBOX) $("div#dashboard a[href$='inbox']").attr('href', linkInbox);
 
        printBadge();
    }
    // on other pages, we check if the stored value is recent enough, otherwise we load it again
    else {
 
        var timeStored = new Date(localStorage.getItem("unread_inbox_date") || '1970'); // the date when the storage was last refreshed
        var timeNow = createDate(0, 0, REFRESH_INTERVAL*-1, 0, 0, 0); // hours before that's max allowed
 
        // if recent enough, simply create the badge
        if (timeStored > timeNow) printBadge();
 
        // if not, we have to start a background load
        else {
            $.get(linkDash, function(response) {
            }).done(function(response) {
 
                // grab the number from within the response
                if ($(response).find("div#dashboard a[href$='inbox']").length > 0) {
                    var badge = $(response).find("div#dashboard a[href$='inbox']").html();
                    badge = badge.match(/\d+/);
 
                    // update the stored data with what we just received
                    localStorage.setItem('unread_inbox_count', badge);
                    localStorage.setItem('unread_inbox_date', new Date());
 
                    printBadge();
                }
                // the response has hit a different page e.g. a CF prompt
                else
                    console.log("[script] Badge for Unread Inbox Messages: ajax error", response);
            }).fail(function(data, textStatus, xhr) {
                //This shows status code eg. 429
                console.log("[script] Badge for Unread Inbox Messages: ajax error", data.status);
            });
        }
    }
 
    // add a little round badge to the user icon in the menu (if there are unread emails)
    // this is called as a function as it needs to run only when the async ajax page load has completed
    function printBadge() {
        const badge = localStorage.getItem('unread_inbox_count');
        const displaytext = (BADGE_ICON) ? `<span class="iconify"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="currentColor"a d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg></span>&nbsp;&nbsp;${badge}`
                                         : `Inbox (${badge})`;
        if (badge != "0") $("#greeting p.icon").prepend(`<a id="inboxbadge" href="${linkInbox}" title="You have unread messages in you inbox">${displaytext}</a>`);
    }
 
})(jQuery);
 
// convenience function to be able to pass minus values into a Date, so JS will automatically shift correctly over month/year boundaries
// thanks to Phil on Stackoverflow for the code snippet https://stackoverflow.com/a/37003268
function createDate(secs, mins, hours, days, months, years) {
    var date = new Date();
    date.setFullYear(date.getFullYear() + years);
    date.setMonth(date.getMonth() + months);
    date.setDate(date.getDate() + days);
    date.setHours(date.getHours() + hours);
    date.setMinutes(date.getMinutes() + mins);
    date.setSeconds(date.getSeconds() + secs);
    return date;
}