// ==UserScript==
// @name GitHub Reactions on lists
// @namespace http://niewiarowski.it/
// @version 0.4.1
// @author marsjaninzmarsa
// @description Delivers shiny emoji reactions to issues and pull requests right to listings!
// @copyright 2017+, Kuba Niewiarowski (niewiarowski.it)
// @license CC-BY-SA-3.0; http://creativecommons.org/licenses/by-sa/3.0/
// @license MIT
// @homepageURL https://github.com/marsjaninzmarsa/userscripts/
// @supportURL https://github.com/marsjaninzmarsa/userscripts/issues
// @match https://github.com/*
// @grant GM_log
// @grant GM_info
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @domain api.github.com
// @require https://code.jquery.com/jquery-3.2.1.js#md5=09dd64a64ba840c31a812a3ca25eaeee,sha384=p7RDedFtQzvcp0/3247fDud39nqze/MUmahi6MOWjyr3WKWaMOyqhXuCT1sM9Q+l
// @require https://update.gf.qytechs.cn/scripts/28721/1108163/mutations.js
// @require https://openuserjs.org/src/libs/cuzi/RequestQueue.js
// @require https://openuserjs.org/src/libs/marsjaninzmarsa/webtoolkit.base64.min.js
// @compatible Firefox with GreaseMonkey
// @compatible Chrome with Tempermonkey
// @compatible Opera with ViolentMonkey
// ==/UserScript==
// ==OpenUserJS==
// @author marsjaninzmarsa
// ==/OpenUserJS==
(function($) {
var rq = new RequestQueue(10);
var uuid = GM_info.uuid || GM_info.script.uuid || GM_getValue('uuid') || GM_setValue('uuid', $.now()) || GM_getValue('uuid');
var username = $('meta[name=user-login]').attr('content');
function process() {
switch(checkMatchers()) {
case "list":
processIssues();
break;
case "tokens":
processTokens();
break;
}
}
function checkMatchers() {
if([
/.+\/.+\/issues\/\d+/i,
/.+\/.+\/pulls\/\d+/i,
].some(function(regexp) {
return regexp.test(window.location.pathname);
})) {
GM_log('Matchers: false');
return false;
}
if([
/.+\/.+\/issues/i,
/.+\/.+\/pulls/i,
].some(function(regexp) {
return regexp.test(window.location.pathname);
})) {
GM_log('Matchers: list');
return "list";
}
if([
/settings\/tokens/i,
].some(function(regexp) {
return regexp.test(window.location.pathname);
})) {
GM_log('Matchers: tokens');
return "tokens";
}
}
function processIssues() {
$('#js-issues-toolbar ~ [role=group] .js-issue-row').each(function() {
processIssue(this);
});
}
function processIssue(issue) {
id = $(issue).data('id');
cached = getDataFromCache(id);
if(!rq.hasReachedTotal()) {
headers = {
"Accept": "application/vnd.github.squirrel-girl-preview",
};
if(cached.etag && !$.isEmptyObject(cached.reactions)) {
headers["If-None-Match"] = cached.etag;
} else if(cached.modified) {
headers["If-Modified-Since"] = cached.modified;
}
if(token = GM_getValue('token')) {
headers["Authorization"] = "Basic "+Base64.encode(token);
}
rq.add({
// GM_log({
method: "GET",
url: "https://api.github.com/repos" + $(issue).find('a.js-navigation-open').attr('href').replace('pull', 'issues') + "/reactions",
responseType: "json",
context: issue,
headers: headers,
onload: function(response) {
response.headers = parseResponseHeaders(response.responseHeaders);
reactions = processResponse(response);
showReactions(response.context, reactions);
}
});
} else {
showReactions(issue, cached.reactions);
}
}
function getDataFromCache(id) {
return JSON.parse(
window.sessionStorage.getItem('githubReactionsUserJs-'+id)
) || {etag: null, modified: null, reactions: {}};
}
function putDataToCache(id, etag, reactions, modified) {
window.sessionStorage.setItem('githubReactionsUserJs-'+id,
JSON.stringify(
{etag: etag, modified: modified, reactions: reactions}
)
);
}
function processResponse(response) {
id = $(response.context).data('id');
cached = getDataFromCache(id);
switch(response.status) {
case 304:
return cached.reactions;
case 401:
processQuotaExceeded(response);
break;
case 403:
if(response.headers['x-ratelimit-remaining'] == 0) {
processQuotaExceeded(response);
}
return cached.reactions;
case 200:
var reactions = {};
if(response.response.length) {
response.response.forEach(function(reaction) {
reactions[reaction.content] = reactions[reaction.content] || [];
reactions[reaction.content].push(reaction.user.login);
});
}
putDataToCache(id, response.headers.etag, reactions, response.headers['last-modified'] || null);
return reactions;
default:
GM_log(response);
break;
}
}
function processQuotaExceeded(response) {
// Abort request and prevent future ones
rq.abort();
rq.maxParallel = 0;
// Explain situation
notification = {
title: "API rate limit exceeded",
text: [
"Quota will reset "+new Date(response.headers['x-ratelimit-reset'] * 1000).toLocaleTimeString()+".",
"You can intercrease limit by providing personal access token."
],
prompt: "Authorize",
highlight: true,
timeout: 0,
onclick: openAccessTokenPage
};
if(response.status == 401) {
notification = $.extend(notification, {
title: "Invalid access token",
text: [
"Access token is invalid and will be reseted.",
"You can generate new token and reauthorize."
],
prompt: "Reauthorize"
});
GM_setValue('token', null);
}
showNotification(notification, response.headers['x-ratelimit-reset']);
showMessage(notification);
// Wait until quota reset and revert
setTimeout(function() {
processQuotaRenewed();
}, response.headers['x-ratelimit-reset'] * 1000 - $.now());
// Maybe token added?
if(typeof GM_addValueChangeListener === 'function') {
GM_addValueChangeListener('token', function() {
processQuotaRenewed();
});
}
var old_value = GM_getValue('token');
var interval = setInterval(function() {
if(GM_getValue('token') != old_value) {
processQuotaRenewed();
clearInterval(interval);
}
}, 10000);
}
function processQuotaRenewed() {
rq.maxParallel = 10;
showMessage(null);
}
// From https://jsperf.com/parse-response-headers-from-xhr/3
function parseResponseHeaders(headerStr) {
var l = headerStr.length,
p = -2,
j = 0,
headers = {},
l, i, q, k, v;
while ( (p = headerStr.indexOf( "\r\n", (i = p + 2) + 5 )) > i )
(q = headerStr.indexOf( ":", i + 3 )) > i && q < p
&& (headers[k = headerStr.slice( i, q ).toLowerCase()] = headerStr.slice( q + 2, p ))[0] === '"'
&& (headers[k] = JSON.parse( headers[k] ));
(q = headerStr.indexOf( ":", i + 3 )) > i && q < l
&& (headers[k = headerStr.slice( i, q ).toLowerCase()] = headerStr.slice( q + 2 ))[0] === '"'
&& (headers[k] = JSON.parse( headers[k] ))
return headers;
}
var tags = [];
function showNotification(notification, tag) {
if(typeof notification === 'string') {
notification = {
text: notification
};
}
if(typeof notification.text === 'object' && notification.text.length) {
notification.text = notification.text.join("\n");
}
notification.title = notification.title || GM_info.script.name;
if(typeof GM_notification === 'function') {
if(tags.indexOf(tag) != -1) {
return;
}
GM_notification(notification);
if(tag) {
tags.push(tag);
}
} else if ("Notification" in window) {
if(Notification.permission === "granted") {
var n = new Notification(notification.title, {
body: notification.text,
tag: tag,
});
if(notification.timeout !== 0) {
setTimeout(n.close.bind(n), notification.timeout || 5000);
}
n.addEventListener('click', notification.onclick);
} else {
Notification.requestPermission(function (permission) {
showNotification(notification, tag);
});
}
} else {
if(tags.indexOf(tag) != -1) {
return;
}
alertText = [notification.title, notification.text].join("\n");
if("onclick" in notification) {
if(confirm(alertText)) {
notification.onclick();
}
} else {
alert(alertText);
}
if(tag) {
tags.push(tag);
}
}
}
function showMessage(message) {
if(typeof message === 'string') {
message = {
text: message
};
}
if(typeof message?.text === 'object' && message.text.length) {
message.text = message.text.join("</span><br /><span>");
}
$('#github-reactions-message').detach();
if(message == null) {
return;
}
$('body').append('<div id="github-reactions-message"></div>');
$('#github-reactions-message').append(
$('#ajax-error-message > svg').clone(),
$('#ajax-error-message > button').clone(),
'<strong>'+(message.title || GM_info.script.name)+':</strong> <span>'+message.text+'</span>',
"\n"
);
if(typeof message.onclick === "function") {
$('#github-reactions-message').append(
'<a href="#">' + (message.prompt || 'Proceed') + '</a>'
);
$('#github-reactions-message a').click(message.onclick);
}
$('#github-reactions-message').addClass('flash flash-warn flash-banner');
}
function openAccessTokenPage() {
GM_openInTab("https://github.com/settings/tokens/new#"+uuid, {
active: true,
insert: true
});
}
function showReactions(issue, reactions) {
if($.isEmptyObject(reactions)) {
return;
}
var container = $(issue).find('.flex-shrink-0 ~ .d-flex > .reactions');
if(container.length) {
$(container).html('');
} else {
wrap = $(issue).find('.flex-shrink-0 ~ .d-flex.no-wrap').removeClass('no-wrap').addClass('flex-wrap');
container = $('<span class="ml-2 flex-shrink-0 d-flex flex-row flex-justify-end pr-2 reactions" style="flex-basis: 100%;"></span>');
wrap.append(container);
}
var emojis = {
"+1": "👍",
"-1": "👎",
"laugh": "😄",
"hooray": "🎉",
"confused": "😕",
"heart": "❤️",
"rocket": "🚀",
"eyes": "👀",
};
$.each(reactions, function(reaction, people) {
const $reaction = $('<span>'+emojis[reaction]+'</span>')
.addClass([
'tooltipped',
'tooltipped-sw',
'tooltipped-multiline',
'tooltipped-align-right-1',
'mt-1',
'social-reaction-summary-item',
// 'btn-link',
// 'no-underline',
].join(' '))
.attr('aria-label', people.join(', ')+' reacted with '+reaction+' emoji')
.append('<span class="text-small text-bold">'+people.length+'</span>');
if(people.includes(username)) {
$reaction.addClass('user-has-reacted');
}
$reaction.appendTo(container);
});
}
function processTokens() {
if(window.location.hash == "#"+uuid) {
window.sessionStorage.setItem('processingTokens', uuid);
window.location.hash = "";
}
if(window.sessionStorage.getItem('processingTokens') == uuid) {
$('#oauth_access_description').val(GM_info.script.name+' userscript in '+GM_info.scriptHandler);
var counter = 0;
$('.js-checkbox-scope').change(function() {
if($(this).is(':checked')) {
var messages = {
0: {
text: "We don't need any of those...",
timeout: 0
},
3: "Nah, srsly, it's just simple quota extension...",
6: "And for what, exactly?",
9: "If you must...",
12: "You're plaing with me, right?",
15: "Nothing here, turn around.",
18: "You're annoing. That's not funny.",
21: "Don't you have anything better to do?",
23: "I don't know, wath some movie, play a game, go outside, find girlfriend... ok, ok, back to Earth, just watch movie.",
28: "Why you don't believe me? You have nothing to do here.",
35: "Looking for porn or what??",
40: "No pron here.",
50: {
title: "Ok, ok, you won. Here, some pussy, have fun.",
text: "[click for cat]",
onclick: function() {
GM_openInTab('https://random.cat/');
},
timeout: 0
}
}
if(messages[counter]) {
showNotification(messages[counter], 'tokenDescription-'+counter);
}
counter = counter+1;
}
});
if($('.access-token.new-token').length) {
showNotification({
text: 'Token generated, save it?',
onclick: saveToken,
});
$('<a href="#" id="github-reactions-save-token-button">Use token in userscript</a>')
.addClass([
'Button',
'Button--small',
'Button--primary',
'BtnGroup-item'
].join(' '))
.click(function(e) {
e.preventDefault();
e.stopPropagation();
saveToken();
})
.prependTo('.access-token.new-token .listgroup-item .float-right');
function saveToken() {
GM_setValue('token', [
username,
$('.access-token.new-token code.token').text()
].join(':'));
$('#github-reactions-save-token-button').text('✓');
showNotification('Token saved!');
showMessage('Token saved, you can close the window.');
}
}
}
}
if(!GM_getValue('hello', false)) {
showNotification({
title: 'Hello!',
text: 'You have succesfully installed GitHub Reactions UserScript. 😊'
}, 'hello');
GM_setValue('hello', true);
}
// GM_log(GM_info);
document.addEventListener("ghmo:container", process);
process();
})(jQuery);