Github User Info

Show inline user information on avatar hover.

As of 2015-04-14. See the latest version.

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