// ==UserScript==
// @name Github User Info
// @id Github_User_Info@https://github.com/jerone/UserScripts
// @namespace https://github.com/jerone/UserScripts
// @description Show inline user information on avatar hover.
// @author jerone
// @copyright 2015+, jerone (https://github.com/jerone)
// @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @homepage https://github.com/jerone/UserScripts/tree/master/Github_User_Info
// @homepageURL https://github.com/jerone/UserScripts/tree/master/Github_User_Info
// @supportURL https://github.com/jerone/UserScripts/issues
// @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VCYMHWQ7ZMBKW
// @icon https://github.githubassets.com/pinned-octocat.svg
// @version 0.4.1
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @run-at document-end
// @include https://github.com/*
// @include https://gist.github.com/*
// ==/UserScript==
// cSpell:ignore leaderboard, vcard, transform
/* eslint security/detect-object-injection: "off" */
(function () {
function proxy(fn) {
return function proxyScope() {
var that = this;
return function proxyEvent(e) {
var args = that.slice(0); // clone
args.unshift(e); // prepend event
fn.apply(this, args);
};
}.call([].slice.call(arguments, 1));
}
var _timer;
var userMenu = document.createElement("div");
userMenu.style =
"display: none;" +
"background-color: #F5F5F5;" +
"border-radius: 3px;" +
"border: 1px solid #DDDDDD;" +
"box-shadow: 0 0 10px rgba(0, 0, 1, 0.1);" +
"font-size: 11px;" +
"padding: 10px;" +
"position: absolute;" +
"width: 335px;" +
"z-index: 99;";
userMenu.classList.add("GithubUserInfo");
userMenu.addEventListener("mouseleave", function mouseleave() {
// console.log('GithubUserInfo:userMenu', 'mouseleave');
window.clearTimeout(_timer);
userMenu.style.display = "none";
});
document.body.appendChild(userMenu);
var userAvatar = document.createElement("a");
userAvatar.style =
"width: 100px;" +
"height: 100px;" +
"float: left;" +
"margin-bottom: 10px;";
userMenu.appendChild(userAvatar);
var userAvatarImg = document.createElement("img");
userAvatarImg.style =
"border-radius: 3px;" +
"transition-property: height, width;" +
"transition-duration: 0.5s;";
userAvatar.appendChild(userAvatarImg);
var userInfo = document.createElement("div");
userInfo.style = "width: 100%;" + "padding-left: 102px;";
userMenu.appendChild(userInfo);
var userName = document.createElement("div");
userName.style =
"padding-left: 24px;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;" +
"font-weight: bold;";
userInfo.appendChild(userName);
var userCompany = document.createElement("div");
userCompany.style =
"display: none;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;";
userInfo.appendChild(userCompany);
var userCompanyIcon = document.createElement("span");
userCompanyIcon.classList.add("octicon", "octicon-organization");
userCompanyIcon.style =
"width: 24px;" + "text-align: center;" + "color: #CCC;";
userCompany.appendChild(userCompanyIcon);
var userCompanyText = document.createElement("span");
userCompany.appendChild(userCompanyText);
var userCompanyAdmin = document.createElement("span");
userCompanyAdmin.style =
"display: none;" +
"margin-left: 5px;" +
"position: relative;" +
"top: -1px;" +
"padding: 2px 5px;" +
"font-size: 10px;" +
"font-weight: bold;" +
"color: #FFF;" +
"text-transform: uppercase;" +
"background-color: #4183C4;" +
"border-radius: 3px;";
userCompanyAdmin.appendChild(document.createTextNode("Staff"));
userCompany.appendChild(userCompanyAdmin);
var userLocation = document.createElement("div");
userLocation.style =
"display: none;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;";
userInfo.appendChild(userLocation);
var userLocationIcon = document.createElement("span");
userLocationIcon.classList.add("octicon", "octicon-location");
userLocationIcon.style =
"width: 24px;" + "text-align: center;" + "color: #CCC;";
userLocation.appendChild(userLocationIcon);
var userLocationText = document.createElement("a");
userLocationText.setAttribute("target", "_blank");
userLocation.appendChild(userLocationText);
var userMail = document.createElement("div");
userMail.style =
"display: none;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;";
userInfo.appendChild(userMail);
var userMailIcon = document.createElement("span");
userMailIcon.classList.add("octicon", "octicon-mail");
userMailIcon.style =
"width: 24px;" + "text-align: center;" + "color: #CCC;";
userMail.appendChild(userMailIcon);
var userMailText = document.createElement("a");
userMail.appendChild(userMailText);
var userLink = document.createElement("div");
userLink.style =
"display: none;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;";
userInfo.appendChild(userLink);
var userLinkIcon = document.createElement("span");
userLinkIcon.classList.add("octicon", "octicon-link");
userLinkIcon.style =
"width: 24px;" + "text-align: center;" + "color: #CCC;";
userLink.appendChild(userLinkIcon);
var userLinkText = document.createElement("a");
userLinkText.setAttribute("target", "_blank");
userLink.appendChild(userLinkText);
var userJoined = document.createElement("div");
userJoined.style =
"display: none;" +
"white-space: nowrap;" +
"overflow: hidden;" +
"text-overflow: ellipsis;";
userInfo.appendChild(userJoined);
var userJoinedIcon = document.createElement("span");
userJoinedIcon.classList.add("octicon", "octicon-clock");
userJoinedIcon.style =
"width: 24px;" + "text-align: center;" + "color: #CCC;";
userJoined.appendChild(userJoinedIcon);
userJoined.appendChild(document.createTextNode("Joined "));
var userJoinedText = unsafeWindow.document.createElement("relative-time"); // https://github.com/github/time-elements
userJoinedText.setAttribute("day", "numeric");
userJoinedText.setAttribute("month", "short");
userJoinedText.setAttribute("year", "numeric");
userJoined.appendChild(userJoinedText);
var userCounts = document.createElement("div");
userCounts.style =
"border-top: 1px solid #EEE;" +
"clear: left;" +
"display: flex;" +
"justify-content: space-around;" +
"text-align: center;" +
"white-space: nowrap;";
userMenu.appendChild(userCounts);
var userFollowers = document.createElement("a");
userFollowers.style = "display: none;" + "text-decoration: none;";
userFollowers.classList.add("vcard-stat");
userFollowers.setAttribute("target", "_blank");
userFollowers.setAttribute("title", "Followers");
userCounts.appendChild(userFollowers);
var userFollowersCount = document.createElement("strong");
userFollowersCount.style = "display: block;" + "font-size: 28px;";
userFollowers.appendChild(userFollowersCount);
var userFollowersText = document.createElement("span");
userFollowersText.appendChild(document.createTextNode("Followers"));
userFollowersText.classList.add("text-muted");
userFollowers.appendChild(userFollowersText);
var userFollowing = document.createElement("a");
userFollowing.style = "display: none;" + "text-decoration: none;";
userFollowing.classList.add("vcard-stat");
userFollowing.setAttribute("target", "_blank");
userFollowing.setAttribute("title", "Following");
userCounts.appendChild(userFollowing);
var userFollowingCount = document.createElement("strong");
userFollowingCount.style = "display: block;" + "font-size: 28px;";
userFollowing.appendChild(userFollowingCount);
var userFollowingText = document.createElement("span");
userFollowingText.appendChild(document.createTextNode("Following"));
userFollowingText.classList.add("text-muted");
userFollowing.appendChild(userFollowingText);
var userRepos = document.createElement("a");
userRepos.style = "display: none;" + "text-decoration: none;";
userRepos.classList.add("vcard-stat");
userRepos.setAttribute("target", "_blank");
userRepos.setAttribute("title", "Public repositories");
userCounts.appendChild(userRepos);
var userReposCount = document.createElement("strong");
userReposCount.style = "display: block;" + "font-size: 28px;";
userRepos.appendChild(userReposCount);
var userReposText = document.createElement("span");
userReposText.appendChild(document.createTextNode("Repos"));
userReposText.classList.add("text-muted");
userRepos.appendChild(userReposText);
var userOrgs = document.createElement("a");
userOrgs.style = "display: none;" + "text-decoration: none;";
userOrgs.classList.add("vcard-stat");
userOrgs.setAttribute("target", "_blank");
userOrgs.setAttribute("title", "Public organizations");
userCounts.appendChild(userOrgs);
var userOrgsCount = document.createElement("strong");
userOrgsCount.style = "display: block;" + "font-size: 28px;";
userOrgs.appendChild(userOrgsCount);
var userOrgsText = document.createElement("span");
userOrgsText.appendChild(document.createTextNode("Orgs"));
userOrgsText.classList.add("text-muted");
userOrgs.appendChild(userOrgsText);
var userMembers = document.createElement("a");
userMembers.style = "display: none;" + "text-decoration: none;";
userMembers.classList.add("vcard-stat");
userMembers.setAttribute("target", "_blank");
userMembers.setAttribute("title", "Public members");
userCounts.appendChild(userMembers);
var userMembersCount = document.createElement("strong");
userMembersCount.style = "display: block;" + "font-size: 28px;";
userMembers.appendChild(userMembersCount);
var userMembersText = document.createElement("span");
userMembersText.appendChild(document.createTextNode("Members"));
userMembersText.classList.add("text-muted");
userMembers.appendChild(userMembersText);
var userGists = document.createElement("a");
userGists.style = "display: none;" + "text-decoration: none;";
userGists.classList.add("vcard-stat");
userGists.setAttribute("target", "_blank");
userGists.setAttribute("title", "Public gists");
userCounts.appendChild(userGists);
var userGistsCount = document.createElement("strong");
userGistsCount.style = "display: block;" + "font-size: 28px;";
userGists.appendChild(userGistsCount);
var userGistsText = document.createElement("span");
userGistsText.appendChild(document.createTextNode("Gists"));
userGistsText.classList.add("text-muted");
userGists.appendChild(userGistsText);
var UPDATE_INTERVAL_DAYS = 7;
function getData(elm) {
var username;
if (elm.getAttribute("alt")) {
username = elm.getAttribute("alt").replace("@", "");
} else if (elm.parentNode.parentNode.querySelector(".author")) {
username = elm.parentNode.parentNode
.querySelector(".author")
.textContent.trim();
} else {
return;
}
var rect = elm.getBoundingClientRect();
var position = {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
};
var avatarSize = {
height: elm.height,
width: elm.width,
};
var usersString = GM_getValue("users", "{}");
var users = JSON.parse(usersString);
if (users[username]) {
var date = new Date(users[username].checked_at),
now = new Date(),
upDate = new Date(
now.setDate(now.getDate() - UPDATE_INTERVAL_DAYS),
);
if (date > upDate) {
var data = users[username].data;
// console.log('GithubUserInfo:getData', 'CACHED', data);
fillData(defaultData(data), position, avatarSize);
} else {
// console.log('GithubUserInfo:getData', 'AJAX - OUTDATED', username, date, upDate);
fetchData(username, position, avatarSize);
}
} else {
// console.log('GithubUserInfo:getData', 'AJAX - NON-EXISTING', username);
fetchData(username, position, avatarSize);
}
}
function fetchData(username, position, avatarSize) {
// console.log('GithubUserInfo:fetchData', username);
GM_xmlhttpRequest({
method: "GET",
url: "https://api.github.com/users/" + username,
onload: proxy(parseUserData, position, avatarSize),
});
}
function parseUserData(response, position, avatarSize) {
var dataParsed = parseRawData(response.responseText);
if (!dataParsed) {
return;
}
var data = defaultData(normalizeData(dataParsed));
// console.log('GithubUserInfo:parseUserData', data.username);
GM_xmlhttpRequest({
method: "GET",
url: "https://api.github.com/users/" + data.username + "/orgs",
onload: proxy(parseOrgsData, position, avatarSize, data),
});
}
function parseOrgsData(response, position, avatarSize, data) {
var dataParsed = parseRawData(response.responseText);
if (!dataParsed) {
return;
}
data.orgs = dataParsed.length;
// console.log('GithubUserInfo:parseOrgsData', data.username, data.orgs);
switch (data.type) {
case "Organization": {
GM_xmlhttpRequest({
method: "GET",
url:
"https://api.github.com/orgs/" +
data.username +
"/members",
onload: proxy(parseMembersData, position, avatarSize, data),
});
break;
}
default: {
fillData(data, position, avatarSize);
setData(data, data.username);
break;
}
}
}
function parseMembersData(response, position, avatarSize, data) {
var dataParsed = parseRawData(response.responseText);
if (!dataParsed) {
return;
}
data.members = dataParsed.length;
// console.log('GithubUserInfo:parseMembersData', data.username, data.members);
fillData(data, position, avatarSize);
setData(data, data.username);
}
function parseRawData(data) {
data = JSON.parse(data);
if (
data.message &&
data.message.startsWith("API rate limit exceeded")
) {
console.warn(
"GithubUserInfo:parseRawData",
"API RATE LIMIT EXCEEDED",
);
return;
}
return data;
}
function normalizeData(data) {
return {
username: data.login,
avatar: data.avatar_url,
type: data.type,
name: data.name,
company: data.company,
blog: data.blog,
location: data.location,
mail: data.email,
repos: data.public_repos,
gists: data.public_gists,
followers: data.followers,
following: data.following,
created_at: data.created_at,
admin: !!data.site_admin,
};
}
function defaultData(data) {
return {
username: data.username,
avatar: data.avatar,
type: data.type,
name: data.name || data.username,
company: data.admin ? "GitHub" : data.company || "",
blog: data.blog || "",
location: data.location || "",
mail: data.mail || "",
repos: data.repos || 0,
gists: data.gists || 0,
followers: data.followers || 0,
following: data.following || 0,
created_at: data.created_at,
admin: data.admin || false,
orgs: data.orgs || 0,
members: data.members || 0,
};
}
function setData(data, username) {
// console.log('GithubUserInfo:setData', username, data);
var usersString = GM_getValue("users", "{}");
var users = JSON.parse(usersString);
users[username] = {
checked_at: new Date().toJSON(),
data: data,
};
GM_setValue("users", JSON.stringify(users));
}
function fillData(data, position, avatarSize) {
// console.log('GithubUserInfo:fillData', data, position, avatarSize);
userAvatar.setAttribute("href", "https://github.com/" + data.username);
userAvatarImg.style.height = avatarSize.height + "px";
userAvatarImg.style.width = avatarSize.width + "px";
userAvatarImg.addEventListener("load", function () {
userMenu.style.top = Math.max(position.top - 10 - 1, 2) + "px";
userMenu.style.left = Math.max(position.left - 10 - 1, 2) + "px";
userMenu.style.display = "block";
window.setTimeout(function avatarAnimationTimeout() {
userAvatarImg.style.height = "100px";
userAvatarImg.style.width = "100px";
}, 50);
});
userAvatarImg.setAttribute("src", "");
userAvatarImg.setAttribute("src", data.avatar);
userName.setAttribute("title", data.username);
userName.textContent = data.name;
if (hasValue(data.company, userCompany)) {
userCompanyText.textContent = data.company;
userCompanyAdmin.style.display = data.admin ? "inline" : "none";
}
if (hasValue(data.location, userLocation)) {
userLocationText.setAttribute(
"href",
"https://maps.google.com/maps?q=" +
encodeURIComponent(data.location),
);
userLocationText.textContent = data.location;
}
if (hasValue(data.mail, userMail)) {
userMailText.setAttribute("href", "mailto:" + data.mail);
userMailText.textContent = data.mail;
}
if (hasValue(data.blog, userLink)) {
userLinkText.setAttribute("href", data.blog);
userLinkText.textContent = data.blog;
}
if (hasValue(data.created_at, userJoined)) {
userJoinedText.setAttribute("datetime", data.created_at);
}
var userCountsHasValue = false;
if (hasValue(data.followers, userFollowers)) {
userCountsHasValue = true;
userFollowers.setAttribute(
"href",
"https://github.com/" + data.username + "/followers",
);
userFollowersCount.textContent = data.followers;
}
if (hasValue(data.following, userFollowing)) {
userCountsHasValue = true;
userFollowing.setAttribute(
"href",
"https://github.com/" + data.username + "/following",
);
userFollowingCount.textContent = data.following;
}
if (hasValue(true, userRepos)) {
// Always show repos count, as long another count is shown too
userRepos.setAttribute(
"href",
"https://github.com/" + data.username + "?tab=repositories",
);
userReposCount.textContent = data.repos;
}
if (hasValue(data.orgs, userOrgs)) {
userCountsHasValue = true;
userOrgs.setAttribute(
"href",
"https://github.com/" + data.username,
);
userOrgsCount.textContent = data.orgs;
}
if (hasValue(data.members, userMembers)) {
userCountsHasValue = true;
userMembers.setAttribute(
"href",
"https://github.com/orgs/" + data.username + "/people",
);
userMembersCount.textContent =
data.members === 30 ? "30+" : data.members;
}
if (hasValue(data.gists, userGists)) {
userCountsHasValue = true;
userGists.setAttribute(
"href",
"https://gist.github.com/" + data.username,
);
userGistsCount.textContent = data.gists;
}
userCounts.style.display = userCountsHasValue ? "flex" : "none";
//if (data.type === 'Organization' || data.type === 'User') {}
}
function hasValue(property, elm) {
elm.style.display = property ? "block" : "none";
return !!property;
}
function init() {
var avatars = document.querySelectorAll(
[
'.avatar[alt^="@"]', // Logged-in user & commits author & issue participant & users organization & organization member
'.avatar-child[alt^="@"]', // Authored committed users
'.gravatar[alt^="@"]', // Following & followers page
'.timeline-comment-avatar[alt^="@"]', // GitHub comments author
'.commits img[alt^="@"]', // Commits on user activity tab
'.leaderboard-gravatar[alt^="@"]', // Trending developer: https://github.com/trending/developers
".gist-author img", // Gist author
".gist .js-discussion .timeline-comment-avatar", // Gist comments author
].join(","),
);
Array.prototype.forEach.call(avatars, function avatarsForEach(avatar) {
avatar.addEventListener("mouseenter", function mouseenter() {
// console.log('GithubUserInfo:avatar', 'mouseenter');
_timer = window.setTimeout(
function mouseenterTimer() {
// console.log('GithubUserInfo:avatar', 'timeout');
getData(this);
}.bind(this),
500,
);
});
avatar.addEventListener("mouseleave", function mouseleave() {
// console.log('GithubUserInfo:avatar', 'mouseleave');
window.clearTimeout(_timer);
});
});
}
// Init
init();
// Pjax
document.addEventListener("pjax:end", init);
})();