Github User Info

Show inline user information on avatar hover.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
})();