AO3: Badge for Unread Inbox Messages

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

  1. // ==UserScript==
  2. // @name AO3: Badge for Unread Inbox Messages
  3. // @namespace https://greasyfork.org/en/users/906106-escctrl
  4. // @version 2.0
  5. // @description puts a little notification badge in the menu for unread messages in your AO3 inbox
  6. // @author escctrl
  7. // @match https://*.archiveofourown.org/*
  8. // @license MIT
  9. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
  10. // @grant none
  11. // ==/UserScript==
  12. /****************** CONFIGURATION ******************/
  13. // how often the script will check for unread messages (in hours)
  14. const REFRESH_INTERVAL = 12;
  15. // if the badge should show as an icon (true), or as text (false)
  16. const BADGE_ICON = true;
  17. // pick a background color for the badge to stand out more, or leave empty quotes ""
  18. const HIGHLIGHT_COLOR = "gold";
  19. // if the inbox link in the sidebar should automatically filter to unread messages only
  20. const FILTER_INBOX = false;
  21. // ****************** NOTE ON LOCAL STORAGE ******************
  22. // For compatibility between userscript managers, this script uses local storage, which is visible from the Developer console.
  23. // If you ever uninstall the script, unfortunately its data can't be automatically deleted.
  24. // If you want to remove the data it sets, (1) visit archiveofourown.org, (2) go into the Developer console,
  25. // (3) look for the Local Storage (4) and delete the entries for "unread_inbox_count" and "unread_inbox_date".
  26. // The script also removes its data if you ever visit AO3 while logged out.
  27. (function($) {
  28. 'use strict';
  29. // first question: is the user logged in? if not, don't bother with any of this
  30. const linkDash = $("#greeting p.icon a").attr('href') || "";
  31. if (linkDash === "") {
  32. localStorage.removeItem('unread_inbox_count');
  33. localStorage.removeItem('unread_inbox_date');
  34. return;
  35. }
  36. var highlight_css = (HIGHLIGHT_COLOR !== "") ? `#greeting #inboxbadge { background-color: ${HIGHLIGHT_COLOR}; border-radius: .25em; }` : "";
  37. $("head").append(`<style type="text/css"> a#inboxbadge .iconify { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
  38. a#inboxbadge { display: block; padding: .25em .75em !important; text-align: center; float: left; margin: 0 1em; line-height: 1.286; height: 1.286em; }
  39. p.icon a { float: right; } ${highlight_css}</style>`);
  40. // build a new inbox link (filtered to unread)
  41. const linkInbox = linkDash + "/inbox?filters[read]=false&filters[replied_to]=all&filters[date]=desc&commit=Filter";
  42. // the fun begins: on a page where we're seeing the unread msgs, we simply set the value
  43. var page_url = window.location.pathname;
  44. if (page_url.includes(linkDash)) {
  45. // grab unread msgs # from the sidebar
  46. var badge = (page_url.includes("/inbox")) ? $("div#dashboard li span.current").html() : $("div#dashboard a[href$='inbox']").html();
  47. badge = badge.match(/\d+/);
  48. // store the currently seen value with the current date, on every page visit, no questions asked
  49. localStorage.setItem('unread_inbox_count', badge);
  50. localStorage.setItem('unread_inbox_date', new Date());
  51. // change sidebar inbox link as well to filtered
  52. if (FILTER_INBOX) $("div#dashboard a[href$='inbox']").attr('href', linkInbox);
  53. printBadge();
  54. }
  55. // on other pages, we check if the stored value is recent enough, otherwise we load it again
  56. else {
  57. var timeStored = new Date(localStorage.getItem("unread_inbox_date") || '1970'); // the date when the storage was last refreshed
  58. var timeNow = createDate(0, 0, REFRESH_INTERVAL*-1, 0, 0, 0); // hours before that's max allowed
  59. // if recent enough, simply create the badge
  60. if (timeStored > timeNow) printBadge();
  61. // if not, we have to start a background load
  62. else {
  63. $.get(linkDash, function(response) {
  64. }).done(function(response) {
  65. // grab the number from within the response
  66. if ($(response).find("div#dashboard a[href$='inbox']").length > 0) {
  67. var badge = $(response).find("div#dashboard a[href$='inbox']").html();
  68. badge = badge.match(/\d+/);
  69. // update the stored data with what we just received
  70. localStorage.setItem('unread_inbox_count', badge);
  71. localStorage.setItem('unread_inbox_date', new Date());
  72. printBadge();
  73. }
  74. // the response has hit a different page e.g. a CF prompt
  75. else
  76. console.log("[script] Badge for Unread Inbox Messages: ajax error", response);
  77. }).fail(function(data, textStatus, xhr) {
  78. //This shows status code eg. 429
  79. console.log("[script] Badge for Unread Inbox Messages: ajax error", data.status);
  80. });
  81. }
  82. }
  83. // add a little round badge to the user icon in the menu (if there are unread emails)
  84. // this is called as a function as it needs to run only when the async ajax page load has completed
  85. function printBadge() {
  86. const badge = localStorage.getItem('unread_inbox_count');
  87. 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}`
  88. : `Inbox (${badge})`;
  89. if (badge != "0") $("#greeting p.icon").prepend(`<a id="inboxbadge" href="${linkInbox}" title="You have unread messages in you inbox">${displaytext}</a>`);
  90. }
  91. })(jQuery);
  92. // convenience function to be able to pass minus values into a Date, so JS will automatically shift correctly over month/year boundaries
  93. // thanks to Phil on Stackoverflow for the code snippet https://stackoverflow.com/a/37003268
  94. function createDate(secs, mins, hours, days, months, years) {
  95. var date = new Date();
  96. date.setFullYear(date.getFullYear() + years);
  97. date.setMonth(date.getMonth() + months);
  98. date.setDate(date.getDate() + days);
  99. date.setHours(date.getHours() + hours);
  100. date.setMinutes(date.getMinutes() + mins);
  101. date.setSeconds(date.getSeconds() + secs);
  102. return date;
  103. }