// ==UserScript==
// @name Add Labels to GitHub Notifications
// @namespace https://gf.qytechs.cn/en/users/668659-denvercoder1
// @match https://github.com/notifications
// @grant none
// @license MIT
// @version 1.0.0
// @author Jonah Lawrence
// @description Use API calls to get the labels of all issues and pull requests from the notification list.
// ==/UserScript==
/*
* Manually clear cache by running the following in the console:
* localStorage.setItem("labels", "{}");
*
* Get more GitHub API requests and enable private repos with a personal access token:
* localStorage.setItem("gh_token", "YOUR_TOKEN_HERE");
*
* To get a personal access token go to https://github.com/settings/tokens/new
* To enable private repos, you will need to enable the repos scope for the token.
*/
(() => {
function hexToRgb(hex) {
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return [r, g, b];
}
function hexToHsl(hex) {
let [r, g, b] = hexToRgb(hex);
r /= 255;
g /= 255;
b /= 255;
const l = Math.max(r, g, b);
const s = l - Math.min(r, g, b);
const h = s ? (l === r ? (g - b) / s : l === g ? 2 + (b - r) / s : 4 + (r - g) / s) : 0;
return [
60 * h < 0 ? 60 * h + 360 : 60 * h,
100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0),
(100 * (2 * l - s)) / 2,
];
}
function addLabels(labels, container) {
// if there are already labels, do nothing
if (container.querySelector(".js-issue-labels")) {
return;
}
// append colored labels to the notification list
const labelContainer = document.createElement("div");
labelContainer.className = "js-issue-labels d-flex flex-wrap";
labelContainer.style.marginTop = "10px";
labelContainer.style.maxHeight = "20px";
labels.forEach((label) => {
const labelElement = document.createElement("span");
labelElement.className = "IssueLabel hx_IssueLabel width-fit mb-1 mr-1 d-inline-flex";
const [r, g, b] = hexToRgb(label.color);
const [h, s, l] = hexToHsl(label.color);
labelElement.setAttribute(
"style",
`--label-r:${r};--label-g:${g};--label-b:${b};--label-h:${h};--label-s:${s};--label-l:${l};`
);
labelElement.innerText = label.name;
labelElement.addEventListener("click", (e) => {
e.stopPropagation();
window.open(label.url);
});
labelContainer.appendChild(labelElement);
});
container.appendChild(labelContainer);
}
function run() {
const cachedLabels = JSON.parse(localStorage.getItem("labels") || "{}");
[...document.querySelectorAll(".notification-list-item-link:not(.added-notifications)")].map((a) => {
a.classList.add("added-notifications");
const url = a.href;
if (cachedLabels[url]) {
console.info("cached", url, cachedLabels[url]);
addLabels(cachedLabels[url], a.parentElement);
return;
}
const issueRegex = /https:\/\/github.com\/(.*)\/(.*)\/(issues|pull)\/(\d+)/;
const match = url.match(issueRegex);
if (match) {
const [, owner, repo, , number] = match;
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`;
const headers = {
Accept: "application/vnd.github.v3+json",
};
const token = localStorage.getItem("gh_token") || "";
if (token) {
headers.Authorization = `token ${token}`;
}
fetch(apiUrl, {
headers,
})
.then((response) => response.json())
.then((data) => {
const labels = data.labels || [];
console.info("fetched", apiUrl, labels);
cachedLabels[url] = labels.map((label) => ({
date: new Date(),
name: label.name,
color: label.color,
url: label.url,
}));
localStorage.setItem("labels", JSON.stringify(cachedLabels));
addLabels(cachedLabels[url], a.parentElement);
})
.catch((error) => console.error(error));
}
});
}
function init() {
// clear cache older than 2 hours
const cachedLabels = JSON.parse(localStorage.getItem("labels") || "{}");
Object.keys(cachedLabels).forEach((url) => {
const { date } = cachedLabels[url];
if (new Date() - new Date(date) > 2 * 60 * 60 * 1000) {
delete cachedLabels[url];
}
});
localStorage.setItem("labels", JSON.stringify(cachedLabels));
// run every 500ms
setInterval(run, 500);
}
// run init when the page loads or if it has already loaded
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();