Github User Info

Show inline user information on avatar hover.

  1. // ==UserScript==
  2. // @name Github User Info
  3. // @id Github_User_Info@https://github.com/jerone/UserScripts
  4. // @namespace https://github.com/jerone/UserScripts
  5. // @description Show inline user information on avatar hover.
  6. // @author jerone
  7. // @copyright 2015+, jerone (https://github.com/jerone)
  8. // @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
  9. // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
  10. // @homepage https://github.com/jerone/UserScripts/tree/master/Github_User_Info
  11. // @homepageURL https://github.com/jerone/UserScripts/tree/master/Github_User_Info
  12. // @supportURL https://github.com/jerone/UserScripts/issues
  13. // @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VCYMHWQ7ZMBKW
  14. // @icon https://github.githubassets.com/pinned-octocat.svg
  15. // @version 0.4.1
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant unsafeWindow
  20. // @run-at document-end
  21. // @include https://github.com/*
  22. // @include https://gist.github.com/*
  23. // ==/UserScript==
  24.  
  25. // cSpell:ignore leaderboard, vcard, transform
  26. /* eslint security/detect-object-injection: "off" */
  27.  
  28. (function () {
  29. function proxy(fn) {
  30. return function proxyScope() {
  31. var that = this;
  32. return function proxyEvent(e) {
  33. var args = that.slice(0); // clone
  34. args.unshift(e); // prepend event
  35. fn.apply(this, args);
  36. };
  37. }.call([].slice.call(arguments, 1));
  38. }
  39.  
  40. var _timer;
  41.  
  42. var userMenu = document.createElement("div");
  43. userMenu.style =
  44. "display: none;" +
  45. "background-color: #F5F5F5;" +
  46. "border-radius: 3px;" +
  47. "border: 1px solid #DDDDDD;" +
  48. "box-shadow: 0 0 10px rgba(0, 0, 1, 0.1);" +
  49. "font-size: 11px;" +
  50. "padding: 10px;" +
  51. "position: absolute;" +
  52. "width: 335px;" +
  53. "z-index: 99;";
  54. userMenu.classList.add("GithubUserInfo");
  55. userMenu.addEventListener("mouseleave", function mouseleave() {
  56. // console.log('GithubUserInfo:userMenu', 'mouseleave');
  57. window.clearTimeout(_timer);
  58. userMenu.style.display = "none";
  59. });
  60. document.body.appendChild(userMenu);
  61.  
  62. var userAvatar = document.createElement("a");
  63. userAvatar.style =
  64. "width: 100px;" +
  65. "height: 100px;" +
  66. "float: left;" +
  67. "margin-bottom: 10px;";
  68. userMenu.appendChild(userAvatar);
  69. var userAvatarImg = document.createElement("img");
  70. userAvatarImg.style =
  71. "border-radius: 3px;" +
  72. "transition-property: height, width;" +
  73. "transition-duration: 0.5s;";
  74. userAvatar.appendChild(userAvatarImg);
  75.  
  76. var userInfo = document.createElement("div");
  77. userInfo.style = "width: 100%;" + "padding-left: 102px;";
  78. userMenu.appendChild(userInfo);
  79.  
  80. var userName = document.createElement("div");
  81. userName.style =
  82. "padding-left: 24px;" +
  83. "white-space: nowrap;" +
  84. "overflow: hidden;" +
  85. "text-overflow: ellipsis;" +
  86. "font-weight: bold;";
  87. userInfo.appendChild(userName);
  88.  
  89. var userCompany = document.createElement("div");
  90. userCompany.style =
  91. "display: none;" +
  92. "white-space: nowrap;" +
  93. "overflow: hidden;" +
  94. "text-overflow: ellipsis;";
  95. userInfo.appendChild(userCompany);
  96. var userCompanyIcon = document.createElement("span");
  97. userCompanyIcon.classList.add("octicon", "octicon-organization");
  98. userCompanyIcon.style =
  99. "width: 24px;" + "text-align: center;" + "color: #CCC;";
  100. userCompany.appendChild(userCompanyIcon);
  101. var userCompanyText = document.createElement("span");
  102. userCompany.appendChild(userCompanyText);
  103. var userCompanyAdmin = document.createElement("span");
  104. userCompanyAdmin.style =
  105. "display: none;" +
  106. "margin-left: 5px;" +
  107. "position: relative;" +
  108. "top: -1px;" +
  109. "padding: 2px 5px;" +
  110. "font-size: 10px;" +
  111. "font-weight: bold;" +
  112. "color: #FFF;" +
  113. "text-transform: uppercase;" +
  114. "background-color: #4183C4;" +
  115. "border-radius: 3px;";
  116. userCompanyAdmin.appendChild(document.createTextNode("Staff"));
  117. userCompany.appendChild(userCompanyAdmin);
  118.  
  119. var userLocation = document.createElement("div");
  120. userLocation.style =
  121. "display: none;" +
  122. "white-space: nowrap;" +
  123. "overflow: hidden;" +
  124. "text-overflow: ellipsis;";
  125. userInfo.appendChild(userLocation);
  126. var userLocationIcon = document.createElement("span");
  127. userLocationIcon.classList.add("octicon", "octicon-location");
  128. userLocationIcon.style =
  129. "width: 24px;" + "text-align: center;" + "color: #CCC;";
  130. userLocation.appendChild(userLocationIcon);
  131. var userLocationText = document.createElement("a");
  132. userLocationText.setAttribute("target", "_blank");
  133. userLocation.appendChild(userLocationText);
  134.  
  135. var userMail = document.createElement("div");
  136. userMail.style =
  137. "display: none;" +
  138. "white-space: nowrap;" +
  139. "overflow: hidden;" +
  140. "text-overflow: ellipsis;";
  141. userInfo.appendChild(userMail);
  142. var userMailIcon = document.createElement("span");
  143. userMailIcon.classList.add("octicon", "octicon-mail");
  144. userMailIcon.style =
  145. "width: 24px;" + "text-align: center;" + "color: #CCC;";
  146. userMail.appendChild(userMailIcon);
  147. var userMailText = document.createElement("a");
  148. userMail.appendChild(userMailText);
  149.  
  150. var userLink = document.createElement("div");
  151. userLink.style =
  152. "display: none;" +
  153. "white-space: nowrap;" +
  154. "overflow: hidden;" +
  155. "text-overflow: ellipsis;";
  156. userInfo.appendChild(userLink);
  157. var userLinkIcon = document.createElement("span");
  158. userLinkIcon.classList.add("octicon", "octicon-link");
  159. userLinkIcon.style =
  160. "width: 24px;" + "text-align: center;" + "color: #CCC;";
  161. userLink.appendChild(userLinkIcon);
  162. var userLinkText = document.createElement("a");
  163. userLinkText.setAttribute("target", "_blank");
  164. userLink.appendChild(userLinkText);
  165.  
  166. var userJoined = document.createElement("div");
  167. userJoined.style =
  168. "display: none;" +
  169. "white-space: nowrap;" +
  170. "overflow: hidden;" +
  171. "text-overflow: ellipsis;";
  172. userInfo.appendChild(userJoined);
  173. var userJoinedIcon = document.createElement("span");
  174. userJoinedIcon.classList.add("octicon", "octicon-clock");
  175. userJoinedIcon.style =
  176. "width: 24px;" + "text-align: center;" + "color: #CCC;";
  177. userJoined.appendChild(userJoinedIcon);
  178. userJoined.appendChild(document.createTextNode("Joined "));
  179. var userJoinedText = unsafeWindow.document.createElement("relative-time"); // https://github.com/github/time-elements
  180. userJoinedText.setAttribute("day", "numeric");
  181. userJoinedText.setAttribute("month", "short");
  182. userJoinedText.setAttribute("year", "numeric");
  183. userJoined.appendChild(userJoinedText);
  184.  
  185. var userCounts = document.createElement("div");
  186. userCounts.style =
  187. "border-top: 1px solid #EEE;" +
  188. "clear: left;" +
  189. "display: flex;" +
  190. "justify-content: space-around;" +
  191. "text-align: center;" +
  192. "white-space: nowrap;";
  193. userMenu.appendChild(userCounts);
  194.  
  195. var userFollowers = document.createElement("a");
  196. userFollowers.style = "display: none;" + "text-decoration: none;";
  197. userFollowers.classList.add("vcard-stat");
  198. userFollowers.setAttribute("target", "_blank");
  199. userFollowers.setAttribute("title", "Followers");
  200. userCounts.appendChild(userFollowers);
  201. var userFollowersCount = document.createElement("strong");
  202. userFollowersCount.style = "display: block;" + "font-size: 28px;";
  203. userFollowers.appendChild(userFollowersCount);
  204. var userFollowersText = document.createElement("span");
  205. userFollowersText.appendChild(document.createTextNode("Followers"));
  206. userFollowersText.classList.add("text-muted");
  207. userFollowers.appendChild(userFollowersText);
  208.  
  209. var userFollowing = document.createElement("a");
  210. userFollowing.style = "display: none;" + "text-decoration: none;";
  211. userFollowing.classList.add("vcard-stat");
  212. userFollowing.setAttribute("target", "_blank");
  213. userFollowing.setAttribute("title", "Following");
  214. userCounts.appendChild(userFollowing);
  215. var userFollowingCount = document.createElement("strong");
  216. userFollowingCount.style = "display: block;" + "font-size: 28px;";
  217. userFollowing.appendChild(userFollowingCount);
  218. var userFollowingText = document.createElement("span");
  219. userFollowingText.appendChild(document.createTextNode("Following"));
  220. userFollowingText.classList.add("text-muted");
  221. userFollowing.appendChild(userFollowingText);
  222.  
  223. var userRepos = document.createElement("a");
  224. userRepos.style = "display: none;" + "text-decoration: none;";
  225. userRepos.classList.add("vcard-stat");
  226. userRepos.setAttribute("target", "_blank");
  227. userRepos.setAttribute("title", "Public repositories");
  228. userCounts.appendChild(userRepos);
  229. var userReposCount = document.createElement("strong");
  230. userReposCount.style = "display: block;" + "font-size: 28px;";
  231. userRepos.appendChild(userReposCount);
  232. var userReposText = document.createElement("span");
  233. userReposText.appendChild(document.createTextNode("Repos"));
  234. userReposText.classList.add("text-muted");
  235. userRepos.appendChild(userReposText);
  236.  
  237. var userOrgs = document.createElement("a");
  238. userOrgs.style = "display: none;" + "text-decoration: none;";
  239. userOrgs.classList.add("vcard-stat");
  240. userOrgs.setAttribute("target", "_blank");
  241. userOrgs.setAttribute("title", "Public organizations");
  242. userCounts.appendChild(userOrgs);
  243. var userOrgsCount = document.createElement("strong");
  244. userOrgsCount.style = "display: block;" + "font-size: 28px;";
  245. userOrgs.appendChild(userOrgsCount);
  246. var userOrgsText = document.createElement("span");
  247. userOrgsText.appendChild(document.createTextNode("Orgs"));
  248. userOrgsText.classList.add("text-muted");
  249. userOrgs.appendChild(userOrgsText);
  250.  
  251. var userMembers = document.createElement("a");
  252. userMembers.style = "display: none;" + "text-decoration: none;";
  253. userMembers.classList.add("vcard-stat");
  254. userMembers.setAttribute("target", "_blank");
  255. userMembers.setAttribute("title", "Public members");
  256. userCounts.appendChild(userMembers);
  257. var userMembersCount = document.createElement("strong");
  258. userMembersCount.style = "display: block;" + "font-size: 28px;";
  259. userMembers.appendChild(userMembersCount);
  260. var userMembersText = document.createElement("span");
  261. userMembersText.appendChild(document.createTextNode("Members"));
  262. userMembersText.classList.add("text-muted");
  263. userMembers.appendChild(userMembersText);
  264.  
  265. var userGists = document.createElement("a");
  266. userGists.style = "display: none;" + "text-decoration: none;";
  267. userGists.classList.add("vcard-stat");
  268. userGists.setAttribute("target", "_blank");
  269. userGists.setAttribute("title", "Public gists");
  270. userCounts.appendChild(userGists);
  271. var userGistsCount = document.createElement("strong");
  272. userGistsCount.style = "display: block;" + "font-size: 28px;";
  273. userGists.appendChild(userGistsCount);
  274. var userGistsText = document.createElement("span");
  275. userGistsText.appendChild(document.createTextNode("Gists"));
  276. userGistsText.classList.add("text-muted");
  277. userGists.appendChild(userGistsText);
  278.  
  279. var UPDATE_INTERVAL_DAYS = 7;
  280.  
  281. function getData(elm) {
  282. var username;
  283. if (elm.getAttribute("alt")) {
  284. username = elm.getAttribute("alt").replace("@", "");
  285. } else if (elm.parentNode.parentNode.querySelector(".author")) {
  286. username = elm.parentNode.parentNode
  287. .querySelector(".author")
  288. .textContent.trim();
  289. } else {
  290. return;
  291. }
  292.  
  293. var rect = elm.getBoundingClientRect();
  294. var position = {
  295. top: rect.top + window.scrollY,
  296. left: rect.left + window.scrollX,
  297. };
  298. var avatarSize = {
  299. height: elm.height,
  300. width: elm.width,
  301. };
  302.  
  303. var usersString = GM_getValue("users", "{}");
  304. var users = JSON.parse(usersString);
  305. if (users[username]) {
  306. var date = new Date(users[username].checked_at),
  307. now = new Date(),
  308. upDate = new Date(
  309. now.setDate(now.getDate() - UPDATE_INTERVAL_DAYS),
  310. );
  311. if (date > upDate) {
  312. var data = users[username].data;
  313. // console.log('GithubUserInfo:getData', 'CACHED', data);
  314. fillData(defaultData(data), position, avatarSize);
  315. } else {
  316. // console.log('GithubUserInfo:getData', 'AJAX - OUTDATED', username, date, upDate);
  317. fetchData(username, position, avatarSize);
  318. }
  319. } else {
  320. // console.log('GithubUserInfo:getData', 'AJAX - NON-EXISTING', username);
  321. fetchData(username, position, avatarSize);
  322. }
  323. }
  324.  
  325. function fetchData(username, position, avatarSize) {
  326. // console.log('GithubUserInfo:fetchData', username);
  327. GM_xmlhttpRequest({
  328. method: "GET",
  329. url: "https://api.github.com/users/" + username,
  330. onload: proxy(parseUserData, position, avatarSize),
  331. });
  332. }
  333.  
  334. function parseUserData(response, position, avatarSize) {
  335. var dataParsed = parseRawData(response.responseText);
  336. if (!dataParsed) {
  337. return;
  338. }
  339. var data = defaultData(normalizeData(dataParsed));
  340. // console.log('GithubUserInfo:parseUserData', data.username);
  341.  
  342. GM_xmlhttpRequest({
  343. method: "GET",
  344. url: "https://api.github.com/users/" + data.username + "/orgs",
  345. onload: proxy(parseOrgsData, position, avatarSize, data),
  346. });
  347. }
  348.  
  349. function parseOrgsData(response, position, avatarSize, data) {
  350. var dataParsed = parseRawData(response.responseText);
  351. if (!dataParsed) {
  352. return;
  353. }
  354. data.orgs = dataParsed.length;
  355. // console.log('GithubUserInfo:parseOrgsData', data.username, data.orgs);
  356.  
  357. switch (data.type) {
  358. case "Organization": {
  359. GM_xmlhttpRequest({
  360. method: "GET",
  361. url:
  362. "https://api.github.com/orgs/" +
  363. data.username +
  364. "/members",
  365. onload: proxy(parseMembersData, position, avatarSize, data),
  366. });
  367. break;
  368. }
  369. default: {
  370. fillData(data, position, avatarSize);
  371. setData(data, data.username);
  372. break;
  373. }
  374. }
  375. }
  376.  
  377. function parseMembersData(response, position, avatarSize, data) {
  378. var dataParsed = parseRawData(response.responseText);
  379. if (!dataParsed) {
  380. return;
  381. }
  382. data.members = dataParsed.length;
  383. // console.log('GithubUserInfo:parseMembersData', data.username, data.members);
  384.  
  385. fillData(data, position, avatarSize);
  386. setData(data, data.username);
  387. }
  388.  
  389. function parseRawData(data) {
  390. data = JSON.parse(data);
  391. if (
  392. data.message &&
  393. data.message.startsWith("API rate limit exceeded")
  394. ) {
  395. console.warn(
  396. "GithubUserInfo:parseRawData",
  397. "API RATE LIMIT EXCEEDED",
  398. );
  399. return;
  400. }
  401. return data;
  402. }
  403.  
  404. function normalizeData(data) {
  405. return {
  406. username: data.login,
  407. avatar: data.avatar_url,
  408. type: data.type,
  409. name: data.name,
  410. company: data.company,
  411. blog: data.blog,
  412. location: data.location,
  413. mail: data.email,
  414. repos: data.public_repos,
  415. gists: data.public_gists,
  416. followers: data.followers,
  417. following: data.following,
  418. created_at: data.created_at,
  419. admin: !!data.site_admin,
  420. };
  421. }
  422.  
  423. function defaultData(data) {
  424. return {
  425. username: data.username,
  426. avatar: data.avatar,
  427. type: data.type,
  428. name: data.name || data.username,
  429. company: data.admin ? "GitHub" : data.company || "",
  430. blog: data.blog || "",
  431. location: data.location || "",
  432. mail: data.mail || "",
  433. repos: data.repos || 0,
  434. gists: data.gists || 0,
  435. followers: data.followers || 0,
  436. following: data.following || 0,
  437. created_at: data.created_at,
  438. admin: data.admin || false,
  439. orgs: data.orgs || 0,
  440. members: data.members || 0,
  441. };
  442. }
  443.  
  444. function setData(data, username) {
  445. // console.log('GithubUserInfo:setData', username, data);
  446. var usersString = GM_getValue("users", "{}");
  447. var users = JSON.parse(usersString);
  448. users[username] = {
  449. checked_at: new Date().toJSON(),
  450. data: data,
  451. };
  452. GM_setValue("users", JSON.stringify(users));
  453. }
  454.  
  455. function fillData(data, position, avatarSize) {
  456. // console.log('GithubUserInfo:fillData', data, position, avatarSize);
  457.  
  458. userAvatar.setAttribute("href", "https://github.com/" + data.username);
  459. userAvatarImg.style.height = avatarSize.height + "px";
  460. userAvatarImg.style.width = avatarSize.width + "px";
  461. userAvatarImg.addEventListener("load", function () {
  462. userMenu.style.top = Math.max(position.top - 10 - 1, 2) + "px";
  463. userMenu.style.left = Math.max(position.left - 10 - 1, 2) + "px";
  464. userMenu.style.display = "block";
  465. window.setTimeout(function avatarAnimationTimeout() {
  466. userAvatarImg.style.height = "100px";
  467. userAvatarImg.style.width = "100px";
  468. }, 50);
  469. });
  470. userAvatarImg.setAttribute("src", "");
  471. userAvatarImg.setAttribute("src", data.avatar);
  472.  
  473. userName.setAttribute("title", data.username);
  474. userName.textContent = data.name;
  475.  
  476. if (hasValue(data.company, userCompany)) {
  477. userCompanyText.textContent = data.company;
  478. userCompanyAdmin.style.display = data.admin ? "inline" : "none";
  479. }
  480. if (hasValue(data.location, userLocation)) {
  481. userLocationText.setAttribute(
  482. "href",
  483. "https://maps.google.com/maps?q=" +
  484. encodeURIComponent(data.location),
  485. );
  486. userLocationText.textContent = data.location;
  487. }
  488. if (hasValue(data.mail, userMail)) {
  489. userMailText.setAttribute("href", "mailto:" + data.mail);
  490. userMailText.textContent = data.mail;
  491. }
  492. if (hasValue(data.blog, userLink)) {
  493. userLinkText.setAttribute("href", data.blog);
  494. userLinkText.textContent = data.blog;
  495. }
  496. if (hasValue(data.created_at, userJoined)) {
  497. userJoinedText.setAttribute("datetime", data.created_at);
  498. }
  499.  
  500. var userCountsHasValue = false;
  501. if (hasValue(data.followers, userFollowers)) {
  502. userCountsHasValue = true;
  503. userFollowers.setAttribute(
  504. "href",
  505. "https://github.com/" + data.username + "/followers",
  506. );
  507. userFollowersCount.textContent = data.followers;
  508. }
  509. if (hasValue(data.following, userFollowing)) {
  510. userCountsHasValue = true;
  511. userFollowing.setAttribute(
  512. "href",
  513. "https://github.com/" + data.username + "/following",
  514. );
  515. userFollowingCount.textContent = data.following;
  516. }
  517. if (hasValue(true, userRepos)) {
  518. // Always show repos count, as long another count is shown too
  519. userRepos.setAttribute(
  520. "href",
  521. "https://github.com/" + data.username + "?tab=repositories",
  522. );
  523. userReposCount.textContent = data.repos;
  524. }
  525. if (hasValue(data.orgs, userOrgs)) {
  526. userCountsHasValue = true;
  527. userOrgs.setAttribute(
  528. "href",
  529. "https://github.com/" + data.username,
  530. );
  531. userOrgsCount.textContent = data.orgs;
  532. }
  533. if (hasValue(data.members, userMembers)) {
  534. userCountsHasValue = true;
  535. userMembers.setAttribute(
  536. "href",
  537. "https://github.com/orgs/" + data.username + "/people",
  538. );
  539. userMembersCount.textContent =
  540. data.members === 30 ? "30+" : data.members;
  541. }
  542. if (hasValue(data.gists, userGists)) {
  543. userCountsHasValue = true;
  544. userGists.setAttribute(
  545. "href",
  546. "https://gist.github.com/" + data.username,
  547. );
  548. userGistsCount.textContent = data.gists;
  549. }
  550. userCounts.style.display = userCountsHasValue ? "flex" : "none";
  551.  
  552. //if (data.type === 'Organization' || data.type === 'User') {}
  553. }
  554.  
  555. function hasValue(property, elm) {
  556. elm.style.display = property ? "block" : "none";
  557. return !!property;
  558. }
  559.  
  560. function init() {
  561. var avatars = document.querySelectorAll(
  562. [
  563. '.avatar[alt^="@"]', // Logged-in user & commits author & issue participant & users organization & organization member
  564. '.avatar-child[alt^="@"]', // Authored committed users
  565. '.gravatar[alt^="@"]', // Following & followers page
  566. '.timeline-comment-avatar[alt^="@"]', // GitHub comments author
  567. '.commits img[alt^="@"]', // Commits on user activity tab
  568. '.leaderboard-gravatar[alt^="@"]', // Trending developer: https://github.com/trending/developers
  569. ".gist-author img", // Gist author
  570. ".gist .js-discussion .timeline-comment-avatar", // Gist comments author
  571. ].join(","),
  572. );
  573. Array.prototype.forEach.call(avatars, function avatarsForEach(avatar) {
  574. avatar.addEventListener("mouseenter", function mouseenter() {
  575. // console.log('GithubUserInfo:avatar', 'mouseenter');
  576. _timer = window.setTimeout(
  577. function mouseenterTimer() {
  578. // console.log('GithubUserInfo:avatar', 'timeout');
  579. getData(this);
  580. }.bind(this),
  581. 500,
  582. );
  583. });
  584. avatar.addEventListener("mouseleave", function mouseleave() {
  585. // console.log('GithubUserInfo:avatar', 'mouseleave');
  586. window.clearTimeout(_timer);
  587. });
  588. });
  589. }
  590.  
  591. // Init
  592. init();
  593.  
  594. // Pjax
  595. document.addEventListener("pjax:end", init);
  596. })();