GitHub Reactions on lists

Delivers shiny emoji reactions to issues and pull requests right to listings!

  1. // ==UserScript==
  2. // @name GitHub Reactions on lists
  3. // @namespace http://niewiarowski.it/
  4. // @version 0.4.1
  5. // @author marsjaninzmarsa
  6. // @description Delivers shiny emoji reactions to issues and pull requests right to listings!
  7. // @copyright 2017+, Kuba Niewiarowski (niewiarowski.it)
  8. // @license CC-BY-SA-3.0; http://creativecommons.org/licenses/by-sa/3.0/
  9. // @license MIT
  10. // @homepageURL https://github.com/marsjaninzmarsa/userscripts/
  11. // @supportURL https://github.com/marsjaninzmarsa/userscripts/issues
  12. // @match https://github.com/*
  13. // @grant GM_log
  14. // @grant GM_info
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_notification
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // @grant GM_addValueChangeListener
  20. // @grant GM_openInTab
  21. // @domain api.github.com
  22. // @require https://code.jquery.com/jquery-3.2.1.js#md5=09dd64a64ba840c31a812a3ca25eaeee,sha384=p7RDedFtQzvcp0/3247fDud39nqze/MUmahi6MOWjyr3WKWaMOyqhXuCT1sM9Q+l
  23. // @require https://update.gf.qytechs.cn/scripts/28721/1108163/mutations.js
  24. // @require https://openuserjs.org/src/libs/cuzi/RequestQueue.js
  25. // @require https://openuserjs.org/src/libs/marsjaninzmarsa/webtoolkit.base64.min.js
  26. // @compatible Firefox with GreaseMonkey
  27. // @compatible Chrome with Tempermonkey
  28. // @compatible Opera with ViolentMonkey
  29. // ==/UserScript==
  30. // ==OpenUserJS==
  31. // @author marsjaninzmarsa
  32. // ==/OpenUserJS==
  33.  
  34. (function($) {
  35. var rq = new RequestQueue(10);
  36. var uuid = GM_info.uuid || GM_info.script.uuid || GM_getValue('uuid') || GM_setValue('uuid', $.now()) || GM_getValue('uuid');
  37. var username = $('meta[name=user-login]').attr('content');
  38.  
  39. function process() {
  40. switch(checkMatchers()) {
  41. case "list":
  42. processIssues();
  43. break;
  44. case "tokens":
  45. processTokens();
  46. break;
  47. }
  48. }
  49.  
  50. function checkMatchers() {
  51. if([
  52. /.+\/.+\/issues\/\d+/i,
  53. /.+\/.+\/pulls\/\d+/i,
  54. ].some(function(regexp) {
  55. return regexp.test(window.location.pathname);
  56. })) {
  57. GM_log('Matchers: false');
  58. return false;
  59. }
  60.  
  61. if([
  62. /.+\/.+\/issues/i,
  63. /.+\/.+\/pulls/i,
  64. ].some(function(regexp) {
  65. return regexp.test(window.location.pathname);
  66. })) {
  67. GM_log('Matchers: list');
  68. return "list";
  69. }
  70.  
  71. if([
  72. /settings\/tokens/i,
  73. ].some(function(regexp) {
  74. return regexp.test(window.location.pathname);
  75. })) {
  76. GM_log('Matchers: tokens');
  77. return "tokens";
  78. }
  79. }
  80.  
  81. function processIssues() {
  82. $('#js-issues-toolbar ~ [role=group] .js-issue-row').each(function() {
  83. processIssue(this);
  84. });
  85. }
  86.  
  87. function processIssue(issue) {
  88. id = $(issue).data('id');
  89. cached = getDataFromCache(id);
  90.  
  91. if(!rq.hasReachedTotal()) {
  92. headers = {
  93. "Accept": "application/vnd.github.squirrel-girl-preview",
  94. };
  95. if(cached.etag && !$.isEmptyObject(cached.reactions)) {
  96. headers["If-None-Match"] = cached.etag;
  97. } else if(cached.modified) {
  98. headers["If-Modified-Since"] = cached.modified;
  99. }
  100. if(token = GM_getValue('token')) {
  101. headers["Authorization"] = "Basic "+Base64.encode(token);
  102. }
  103. rq.add({
  104. // GM_log({
  105. method: "GET",
  106. url: "https://api.github.com/repos" + $(issue).find('a.js-navigation-open').attr('href').replace('pull', 'issues') + "/reactions",
  107. responseType: "json",
  108. context: issue,
  109. headers: headers,
  110. onload: function(response) {
  111. response.headers = parseResponseHeaders(response.responseHeaders);
  112. reactions = processResponse(response);
  113. showReactions(response.context, reactions);
  114. }
  115. });
  116. } else {
  117. showReactions(issue, cached.reactions);
  118. }
  119. }
  120.  
  121. function getDataFromCache(id) {
  122. return JSON.parse(
  123. window.sessionStorage.getItem('githubReactionsUserJs-'+id)
  124. ) || {etag: null, modified: null, reactions: {}};
  125. }
  126.  
  127. function putDataToCache(id, etag, reactions, modified) {
  128. window.sessionStorage.setItem('githubReactionsUserJs-'+id,
  129. JSON.stringify(
  130. {etag: etag, modified: modified, reactions: reactions}
  131. )
  132. );
  133. }
  134.  
  135. function processResponse(response) {
  136. id = $(response.context).data('id');
  137. cached = getDataFromCache(id);
  138.  
  139. switch(response.status) {
  140. case 304:
  141. return cached.reactions;
  142. case 401:
  143. processQuotaExceeded(response);
  144. break;
  145. case 403:
  146. if(response.headers['x-ratelimit-remaining'] == 0) {
  147. processQuotaExceeded(response);
  148. }
  149. return cached.reactions;
  150. case 200:
  151. var reactions = {};
  152. if(response.response.length) {
  153. response.response.forEach(function(reaction) {
  154. reactions[reaction.content] = reactions[reaction.content] || [];
  155. reactions[reaction.content].push(reaction.user.login);
  156. });
  157. }
  158. putDataToCache(id, response.headers.etag, reactions, response.headers['last-modified'] || null);
  159. return reactions;
  160. default:
  161. GM_log(response);
  162. break;
  163. }
  164. }
  165.  
  166. function processQuotaExceeded(response) {
  167. // Abort request and prevent future ones
  168. rq.abort();
  169. rq.maxParallel = 0;
  170.  
  171. // Explain situation
  172. notification = {
  173. title: "API rate limit exceeded",
  174. text: [
  175. "Quota will reset "+new Date(response.headers['x-ratelimit-reset'] * 1000).toLocaleTimeString()+".",
  176. "You can intercrease limit by providing personal access token."
  177. ],
  178. prompt: "Authorize",
  179. highlight: true,
  180. timeout: 0,
  181. onclick: openAccessTokenPage
  182. };
  183. if(response.status == 401) {
  184. notification = $.extend(notification, {
  185. title: "Invalid access token",
  186. text: [
  187. "Access token is invalid and will be reseted.",
  188. "You can generate new token and reauthorize."
  189. ],
  190. prompt: "Reauthorize"
  191. });
  192. GM_setValue('token', null);
  193. }
  194. showNotification(notification, response.headers['x-ratelimit-reset']);
  195. showMessage(notification);
  196.  
  197. // Wait until quota reset and revert
  198. setTimeout(function() {
  199. processQuotaRenewed();
  200. }, response.headers['x-ratelimit-reset'] * 1000 - $.now());
  201.  
  202. // Maybe token added?
  203. if(typeof GM_addValueChangeListener === 'function') {
  204. GM_addValueChangeListener('token', function() {
  205. processQuotaRenewed();
  206. });
  207. }
  208. var old_value = GM_getValue('token');
  209. var interval = setInterval(function() {
  210. if(GM_getValue('token') != old_value) {
  211. processQuotaRenewed();
  212. clearInterval(interval);
  213. }
  214. }, 10000);
  215. }
  216.  
  217. function processQuotaRenewed() {
  218. rq.maxParallel = 10;
  219. showMessage(null);
  220. }
  221.  
  222. // From https://jsperf.com/parse-response-headers-from-xhr/3
  223. function parseResponseHeaders(headerStr) {
  224. var l = headerStr.length,
  225. p = -2,
  226. j = 0,
  227. headers = {},
  228. l, i, q, k, v;
  229.  
  230. while ( (p = headerStr.indexOf( "\r\n", (i = p + 2) + 5 )) > i )
  231. (q = headerStr.indexOf( ":", i + 3 )) > i && q < p
  232. && (headers[k = headerStr.slice( i, q ).toLowerCase()] = headerStr.slice( q + 2, p ))[0] === '"'
  233. && (headers[k] = JSON.parse( headers[k] ));
  234. (q = headerStr.indexOf( ":", i + 3 )) > i && q < l
  235. && (headers[k = headerStr.slice( i, q ).toLowerCase()] = headerStr.slice( q + 2 ))[0] === '"'
  236. && (headers[k] = JSON.parse( headers[k] ))
  237. return headers;
  238. }
  239.  
  240. var tags = [];
  241. function showNotification(notification, tag) {
  242. if(typeof notification === 'string') {
  243. notification = {
  244. text: notification
  245. };
  246. }
  247. if(typeof notification.text === 'object' && notification.text.length) {
  248. notification.text = notification.text.join("\n");
  249. }
  250. notification.title = notification.title || GM_info.script.name;
  251.  
  252. if(typeof GM_notification === 'function') {
  253. if(tags.indexOf(tag) != -1) {
  254. return;
  255. }
  256. GM_notification(notification);
  257. if(tag) {
  258. tags.push(tag);
  259. }
  260. } else if ("Notification" in window) {
  261. if(Notification.permission === "granted") {
  262. var n = new Notification(notification.title, {
  263. body: notification.text,
  264. tag: tag,
  265. });
  266. if(notification.timeout !== 0) {
  267. setTimeout(n.close.bind(n), notification.timeout || 5000);
  268. }
  269. n.addEventListener('click', notification.onclick);
  270. } else {
  271. Notification.requestPermission(function (permission) {
  272. showNotification(notification, tag);
  273. });
  274. }
  275. } else {
  276. if(tags.indexOf(tag) != -1) {
  277. return;
  278. }
  279. alertText = [notification.title, notification.text].join("\n");
  280. if("onclick" in notification) {
  281. if(confirm(alertText)) {
  282. notification.onclick();
  283. }
  284. } else {
  285. alert(alertText);
  286. }
  287. if(tag) {
  288. tags.push(tag);
  289. }
  290. }
  291. }
  292.  
  293. function showMessage(message) {
  294. if(typeof message === 'string') {
  295. message = {
  296. text: message
  297. };
  298. }
  299. if(typeof message?.text === 'object' && message.text.length) {
  300. message.text = message.text.join("</span><br /><span>");
  301. }
  302.  
  303. $('#github-reactions-message').detach();
  304. if(message == null) {
  305. return;
  306. }
  307. $('body').append('<div id="github-reactions-message"></div>');
  308. $('#github-reactions-message').append(
  309. $('#ajax-error-message > svg').clone(),
  310. $('#ajax-error-message > button').clone(),
  311. '<strong>'+(message.title || GM_info.script.name)+':</strong> <span>'+message.text+'</span>',
  312. "\n"
  313. );
  314. if(typeof message.onclick === "function") {
  315. $('#github-reactions-message').append(
  316. '<a href="#">' + (message.prompt || 'Proceed') + '</a>'
  317. );
  318. $('#github-reactions-message a').click(message.onclick);
  319. }
  320. $('#github-reactions-message').addClass('flash flash-warn flash-banner');
  321. }
  322.  
  323. function openAccessTokenPage() {
  324. GM_openInTab("https://github.com/settings/tokens/new#"+uuid, {
  325. active: true,
  326. insert: true
  327. });
  328. }
  329.  
  330. function showReactions(issue, reactions) {
  331. if($.isEmptyObject(reactions)) {
  332. return;
  333. }
  334. var container = $(issue).find('.flex-shrink-0 ~ .d-flex > .reactions');
  335. if(container.length) {
  336. $(container).html('');
  337. } else {
  338. wrap = $(issue).find('.flex-shrink-0 ~ .d-flex.no-wrap').removeClass('no-wrap').addClass('flex-wrap');
  339. container = $('<span class="ml-2 flex-shrink-0 d-flex flex-row flex-justify-end pr-2 reactions" style="flex-basis: 100%;"></span>');
  340. wrap.append(container);
  341. }
  342. var emojis = {
  343. "+1": "👍",
  344. "-1": "👎",
  345. "laugh": "😄",
  346. "hooray": "🎉",
  347. "confused": "😕",
  348. "heart": "❤️",
  349. "rocket": "🚀",
  350. "eyes": "👀",
  351. };
  352. $.each(reactions, function(reaction, people) {
  353. const $reaction = $('<span>'+emojis[reaction]+'</span>')
  354. .addClass([
  355. 'tooltipped',
  356. 'tooltipped-sw',
  357. 'tooltipped-multiline',
  358. 'tooltipped-align-right-1',
  359. 'mt-1',
  360. 'social-reaction-summary-item',
  361. // 'btn-link',
  362. // 'no-underline',
  363.  
  364. ].join(' '))
  365. .attr('aria-label', people.join(', ')+' reacted with '+reaction+' emoji')
  366. .append('<span class="text-small text-bold">'+people.length+'</span>');
  367. if(people.includes(username)) {
  368. $reaction.addClass('user-has-reacted');
  369. }
  370. $reaction.appendTo(container);
  371. });
  372. }
  373.  
  374. function processTokens() {
  375. if(window.location.hash == "#"+uuid) {
  376. window.sessionStorage.setItem('processingTokens', uuid);
  377. window.location.hash = "";
  378. }
  379. if(window.sessionStorage.getItem('processingTokens') == uuid) {
  380. $('#oauth_access_description').val(GM_info.script.name+' userscript in '+GM_info.scriptHandler);
  381. var counter = 0;
  382. $('.js-checkbox-scope').change(function() {
  383. if($(this).is(':checked')) {
  384. var messages = {
  385. 0: {
  386. text: "We don't need any of those...",
  387. timeout: 0
  388. },
  389. 3: "Nah, srsly, it's just simple quota extension...",
  390. 6: "And for what, exactly?",
  391. 9: "If you must...",
  392. 12: "You're plaing with me, right?",
  393. 15: "Nothing here, turn around.",
  394. 18: "You're annoing. That's not funny.",
  395. 21: "Don't you have anything better to do?",
  396. 23: "I don't know, wath some movie, play a game, go outside, find girlfriend... ok, ok, back to Earth, just watch movie.",
  397. 28: "Why you don't believe me? You have nothing to do here.",
  398. 35: "Looking for porn or what??",
  399. 40: "No pron here.",
  400. 50: {
  401. title: "Ok, ok, you won. Here, some pussy, have fun.",
  402. text: "[click for cat]",
  403. onclick: function() {
  404. GM_openInTab('https://random.cat/');
  405. },
  406. timeout: 0
  407. }
  408. }
  409. if(messages[counter]) {
  410. showNotification(messages[counter], 'tokenDescription-'+counter);
  411. }
  412. counter = counter+1;
  413. }
  414. });
  415.  
  416. if($('.access-token.new-token').length) {
  417. showNotification({
  418. text: 'Token generated, save it?',
  419. onclick: saveToken,
  420. });
  421. $('<a href="#" id="github-reactions-save-token-button">Use token in userscript</a>')
  422. .addClass([
  423. 'Button',
  424. 'Button--small',
  425. 'Button--primary',
  426. 'BtnGroup-item'
  427. ].join(' '))
  428. .click(function(e) {
  429. e.preventDefault();
  430. e.stopPropagation();
  431. saveToken();
  432. })
  433. .prependTo('.access-token.new-token .listgroup-item .float-right');
  434.  
  435. function saveToken() {
  436. GM_setValue('token', [
  437. username,
  438. $('.access-token.new-token code.token').text()
  439. ].join(':'));
  440. $('#github-reactions-save-token-button').text('✓');
  441. showNotification('Token saved!');
  442. showMessage('Token saved, you can close the window.');
  443. }
  444. }
  445. }
  446. }
  447.  
  448.  
  449.  
  450. if(!GM_getValue('hello', false)) {
  451. showNotification({
  452. title: 'Hello!',
  453. text: 'You have succesfully installed GitHub Reactions UserScript. 😊'
  454. }, 'hello');
  455. GM_setValue('hello', true);
  456. }
  457.  
  458.  
  459. // GM_log(GM_info);
  460.  
  461. document.addEventListener("ghmo:container", process);
  462.  
  463. process();
  464.  
  465. })(jQuery);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址