Add Labels to GitHub Notifications

Use API calls to get the labels of all issues and pull requests from the notification list.

  1. // ==UserScript==
  2. // @name Add Labels to GitHub Notifications
  3. // @namespace https://greasyfork.org/en/users/668659-denvercoder1
  4. // @match https://github.com/notifications
  5. // @grant none
  6. // @license MIT
  7. // @version 1.0.3
  8. // @author Jonah Lawrence
  9. // @description Use API calls to get the labels of all issues and pull requests from the notification list.
  10. // ==/UserScript==
  11.  
  12. /* jshint esversion: 11 */
  13.  
  14. /*
  15. * Get more GitHub API requests and enable private repos with a personal access token:
  16. * localStorage.setItem("gh_token", "YOUR_TOKEN_HERE");
  17. *
  18. * To get a personal access token go to https://github.com/settings/tokens/new
  19. * To enable private repos, you will need to enable the repos scope for the token.
  20. *
  21. * Manually clear cache by running the following in the console:
  22. * localStorage.setItem("labels", "{}");
  23. */
  24.  
  25. (() => {
  26. // cached labels
  27. let cachedLabels = {};
  28.  
  29. /**
  30. * Convert a hex color to RGB
  31. * @param {string} hex - hex color (eg. FFFFFF)
  32. * @returns {number[]} [r, g, b]
  33. */
  34. function hexToRgb(hex) {
  35. const bigint = parseInt(hex, 16);
  36. const r = (bigint >> 16) & 255;
  37. const g = (bigint >> 8) & 255;
  38. const b = bigint & 255;
  39. return [r, g, b];
  40. }
  41.  
  42. /**
  43. * Convert a hex color to HSL
  44. * @param {string} hex - hex color (eg. FFFFFF)
  45. * @returns {number[]} [h, s, l]
  46. */
  47. function hexToHsl(hex) {
  48. let [r, g, b] = hexToRgb(hex);
  49. r /= 255;
  50. g /= 255;
  51. b /= 255;
  52. const l = Math.max(r, g, b);
  53. const s = l - Math.min(r, g, b);
  54. const h = s ? (l === r ? (g - b) / s : l === g ? 2 + (b - r) / s : 4 + (r - g) / s) : 0;
  55. return [
  56. 60 * h < 0 ? 60 * h + 360 : 60 * h,
  57. 100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0),
  58. (100 * (2 * l - s)) / 2,
  59. ];
  60. }
  61.  
  62. /**
  63. * Add labels to the notification list
  64. * @param {object[]} labels - array of label objects
  65. * @param {HTMLElement} container - parent element to append labels to
  66. */
  67. function addLabels(labels, container) {
  68. // if there are already labels, do nothing
  69. if (container.querySelector(".js-issue-labels")) {
  70. return;
  71. }
  72. // append colored labels to the notification list
  73. const labelContainer = document.createElement("div");
  74. labelContainer.className = "js-issue-labels d-flex flex-wrap";
  75. labelContainer.style.marginTop = "10px";
  76. labelContainer.style.maxHeight = "20px";
  77. labels.forEach((label) => {
  78. const labelElement = document.createElement("span");
  79. labelElement.className = "IssueLabel hx_IssueLabel width-fit mb-1 mr-1 d-inline-flex";
  80. const [r, g, b] = hexToRgb(label.color);
  81. const [h, s, l] = hexToHsl(label.color);
  82. labelElement.setAttribute(
  83. "style",
  84. `--label-r:${r};--label-g:${g};--label-b:${b};--label-h:${h};--label-s:${s};--label-l:${l}; cursor:pointer;`
  85. );
  86. labelElement.innerText = label.name;
  87. labelElement.addEventListener("click", (e) => {
  88. e.stopPropagation();
  89. window.open(label.filterUrl);
  90. });
  91. labelContainer.appendChild(labelElement);
  92. });
  93. container.appendChild(labelContainer);
  94. }
  95.  
  96. /**
  97. * Fetch labels from the GitHub API and add them to the cache for an issue or pull request given its url
  98. * @param {string} url - url of the issue or pull request
  99. * @param {HTMLElement|null} container - parent element to append labels to (optional)
  100. */
  101. function fetchLabels(url, container) {
  102. const issueRegex = /https:\/\/github.com\/(.*)\/(.*)\/(issues|pull)\/(\d+)/;
  103. const match = url.match(issueRegex);
  104. if (match) {
  105. const [, owner, repo, , number] = match;
  106. const apiUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`;
  107. const repoIssuesUrl = `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`;
  108. const headers = {
  109. Accept: "application/vnd.github.v3+json",
  110. };
  111. const token = localStorage.getItem("gh_token") || "";
  112. if (token) {
  113. headers.Authorization = `token ${token}`;
  114. }
  115. fetch(apiUrl, {
  116. headers,
  117. })
  118. .then((response) => response.json())
  119. .then((data) => {
  120. const labels = data.labels || [];
  121. cachedLabels[url] = {
  122. date: new Date(),
  123. labels: labels.map((label) => ({
  124. name: label.name,
  125. color: label.color,
  126. filterUrl: `${repoIssuesUrl}?q=is%3Aopen+label%3A"${encodeURIComponent(label.name)}"`,
  127. })),
  128. };
  129. console.info("fetched", url, cachedLabels[url]);
  130. localStorage.setItem("labels", JSON.stringify(cachedLabels));
  131. if (container) {
  132. addLabels(cachedLabels[url].labels, container);
  133. }
  134. })
  135. .catch((error) => console.error(error));
  136. }
  137. }
  138.  
  139. /**
  140. * Check the notification list for new issues and pull requests and add labels to them
  141. */
  142. function run() {
  143. const notificationLinks = [
  144. ...document.querySelectorAll(".notification-list-item-link:not(.added-notifications)"),
  145. ];
  146. if (notificationLinks.length === 0) {
  147. return;
  148. }
  149. notificationLinks.forEach((a) => {
  150. a.classList.add("added-notifications");
  151. const url = a.href;
  152. const container = a.parentElement;
  153. // use cached labels if they exist and the notification last update is older than the fetch date of the labels
  154. const updatedDate = container.parentElement.querySelector("relative-time")?.getAttribute("datetime");
  155. if (cachedLabels[url] && new Date(updatedDate) < new Date(cachedLabels[url].date)) {
  156. console.info("cached", url, cachedLabels[url]);
  157. addLabels(cachedLabels[url].labels || [], container);
  158. return;
  159. }
  160. // otherwise fetch the labels from the GitHub API
  161. fetchLabels(url, container);
  162. });
  163. }
  164.  
  165. function init() {
  166. // clear cache older than 6 hours
  167. cachedLabels = JSON.parse(localStorage.getItem("labels") || "{}");
  168. Object.keys(cachedLabels).forEach((url) => {
  169. const { date } = cachedLabels[url];
  170. if (new Date() - new Date(date) > 1000 * 60 * 60 * 6) {
  171. delete cachedLabels[url];
  172. }
  173. });
  174. localStorage.setItem("labels", JSON.stringify(cachedLabels));
  175.  
  176. // run every 500ms
  177. setInterval(run, 500);
  178. }
  179.  
  180. // run init when the page loads or if it has already loaded
  181. if (document.readyState === "loading") {
  182. document.addEventListener("DOMContentLoaded", init);
  183. } else {
  184. init();
  185. }
  186. })();