Last active
December 1, 2022 13:04
-
-
Save dallasread/82e3263ab3344909b3b87ea7c1c550d8 to your computer and use it in GitHub Desktop.
github-commenter.user.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name GitHubCommenter | |
// @version 1 | |
// @grant none | |
// @match https://dnsimple.com/admin* | |
// ==/UserScript== | |
class Commenter { | |
constructor($target, namespace, repo) { | |
this.repo = repo | |
this.namespace = namespace | |
this.accessToken = null | |
this.cookieName = `${this.namespace}-access-token` | |
this.appendElements($target) | |
this.init() | |
} | |
init() { | |
const accessToken = localStorage.getItem(this.cookieName) | |
if (accessToken) { | |
this.accessToken = accessToken | |
this.fetch() | |
} | |
} | |
appendElements($target) { | |
const $style = document.createElement('style') | |
$style.innerHTML = ` | |
@keyframes fadeslideUp { | |
0% { | |
-webkit-transform: translate3d(0, 100%, 0); | |
transform: translate3d(0, 100%, 0); | |
visibility: visible; | |
} | |
100% { | |
-webkit-transform: translateZ(0); | |
transform: translateZ(0); | |
} | |
} | |
.${this.namespace} { | |
position: fixed; | |
bottom: 0; | |
left: 0; | |
line-height: 1.6; | |
box-sizing: border-box; | |
z-index: 9999999; | |
} | |
.${this.namespace} *, .${this.namespace} *::before, .${this.namespace} *::after { | |
box-sizing: inherit; | |
} | |
.${this.namespace} .${this.namespace}-overlay { | |
position: fixed; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
z-index: 9999999; | |
} | |
.${this.namespace} .${this.namespace}-prompt { | |
position: fixed; | |
left: 0.5rem; | |
bottom: 0.5rem; | |
} | |
.${this.namespace} .${this.namespace}-prompt svg { | |
transform: scaleX(-1); | |
display: block; | |
width: 2.5rem; | |
height: 2.5rem; | |
} | |
.${this.namespace} .${this.namespace}-prompt .${this.namespace}-counter { | |
display: block; | |
width: 1.25rem; | |
height: 1.25rem; | |
position: absolute; | |
bottom: 1.25rem; | |
left: 1.75rem; | |
border-radius: 50%; | |
background: #1f6feb; | |
color: #fff; | |
text-align: center; | |
font-size: 0.6rem; | |
padding-top: 0.1rem; | |
} | |
.${this.namespace} .${this.namespace}-header { | |
background: #eee; | |
padding: 1rem; | |
border-bottom: 1px solid #ddd; | |
} | |
.${this.namespace} .${this.namespace}-header h3 { | |
margin: 0; | |
line-height: 1; | |
} | |
.${this.namespace} .${this.namespace}-header .${this.namespace}-html-url { | |
display: inline-block; | |
margin-left: 0.5rem; | |
} | |
.${this.namespace} .${this.namespace}-header .${this.namespace}-forget-link { | |
font-size: 0.75rem; | |
float: right; | |
} | |
.${this.namespace} form label { | |
display: block; | |
} | |
.${this.namespace} form { | |
margin: 0; | |
line-height: 1; | |
} | |
.${this.namespace} input { | |
width: 100%; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
padding: 0.5rem; | |
} | |
.${this.namespace} .${this.namespace}-login button { | |
width: 100%; | |
margin-top: 1rem; | |
} | |
.${this.namespace} .${this.namespace}-page { | |
position: fixed; | |
bottom: 0; | |
left: 0; | |
width: 30rem; | |
background: #fff; | |
border-top-right-radius: 4px; | |
border-top: 1px solid #ddd; | |
border-right: 1px solid #ddd; | |
z-index: 10000000; | |
animation: fadeslideUp; | |
-webkit-animation-duration: 500ms; | |
animation-duration: 500ms; | |
-webkit-animation-fill-mode: both; | |
animation-fill-mode: both; | |
} | |
.${this.namespace} .${this.namespace}-comments { | |
position: relative; | |
background: #fafafa; | |
} | |
.${this.namespace} .${this.namespace}-comments form textarea { | |
width: 100%; | |
border: 0; | |
height: 6rem; | |
padding: 0.5rem; | |
border-top: 1px solid #ddd; | |
outline: 0 !important; | |
resize: none; | |
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);. | |
background: #fff; | |
line-height: 1.6; | |
} | |
.${this.namespace} .${this.namespace}-comments form button { | |
position: absolute; | |
bottom: 5px; | |
right: 5px; | |
} | |
.${this.namespace} button { | |
background: #1f6feb; | |
color: #fff; | |
border-radius: 4px; | |
padding: 0.25rem 0.75rem; | |
border: 0; | |
display: block; | |
line-height: 1.6; | |
} | |
.${this.namespace} .${this.namespace}-page form[disabled] button { | |
opacity: 0.5; | |
content: 'Loading...' | |
} | |
.${this.namespace} .${this.namespace}-with-padding { | |
padding: 1rem; | |
} | |
.${this.namespace} .${this.namespace}-comments ul { | |
margin: 0; | |
padding: 0px; | |
list-style: none; | |
max-height: 60vh; | |
overflow-y: auto; | |
margin-bottom: -1px; | |
} | |
.${this.namespace} .${this.namespace}-comments ul li { | |
margin: 0; | |
padding: 0px; | |
border-bottom: 1px solid #ddd; | |
padding: 1rem; | |
} | |
.${this.namespace} .${this.namespace}-comments ul li:last-child { | |
border-bottom: 0; | |
} | |
.${this.namespace} .${this.namespace}-author { | |
font-size: 0.8rem; | |
opacity: 0.8; | |
} | |
.${this.namespace} p { | |
margin: 0; | |
} | |
` | |
this.$commenter = document.createElement('div') | |
this.$commenter.className = this.namespace | |
this.$overlay = document.createElement('div') | |
this.$overlay.className = `${this.namespace}-overlay` | |
this.$overlay.style.display = 'none' | |
this.$overlay.onclick = () => this.close() | |
this.$commenter.appendChild(this.$overlay) | |
this.$prompt = document.createElement('a') | |
this.$prompt.href = 'javascript:;' | |
this.$prompt.className = `${this.namespace}-prompt` | |
this.$prompt.innerHTML = Commenter.ICON('#ccc') | |
this.$prompt.onclick = () => this.toggle() | |
this.$counter = document.createElement('span') | |
this.$counter.className = `${this.namespace}-counter` | |
this.$counter.innerText = '?' | |
this.$prompt.appendChild(this.$counter) | |
this.$commenter.appendChild(this.$prompt) | |
this.$login = document.createElement('form') | |
this.$login.className = `${this.namespace}-login ${this.namespace}-page ${this.namespace}-with-padding` | |
this.$login.style.display = 'none' | |
const $accessToken = document.createElement('input') | |
$accessToken.type = 'password' | |
const $accessTokenLabel = document.createElement('label') | |
$accessTokenLabel.innerHTML = 'GitHub Access Token (<a href="https://github.com/settings/tokens" target="_blank">info</a>)' | |
$accessTokenLabel.for = $accessToken | |
const $button = document.createElement('button') | |
$button.innerText = 'Log in' | |
$button.type = 'submit' | |
this.$login.onsubmit = (event) => { | |
event.preventDefault(); | |
this.updateAccessToken($accessToken.value) | |
this.$login.reset() | |
this.go('$comments') | |
} | |
this.$login.onMounted = () => { | |
$accessToken.focus() | |
} | |
this.$login.appendChild($accessTokenLabel) | |
this.$login.appendChild($accessToken) | |
this.$login.appendChild($button) | |
this.$commenter.appendChild(this.$login) | |
this.$comments = document.createElement('div') | |
this.$comments.className = `${this.namespace}-comments ${this.namespace}-page` | |
this.$commentsHeader = document.createElement('div') | |
this.$commentsHeader.className = `${this.namespace}-header` | |
this.$commentsHeaderTitle = document.createElement('h3') | |
this.$commentsHeaderTitle.innerText = 'Comments' | |
this.$commentsHeaderTitleLink = document.createElement('a') | |
this.$commentsHeaderTitleLink.className = `${this.namespace}-html-url` | |
this.$commentsHeaderTitleLink.style.display = 'none' | |
this.$commentsHeaderTitleLink.target = '_blank' | |
this.$commentsHeaderTitleLink.innerHTML = '🔗' | |
this.$commentsHeaderTitle.appendChild(this.$commentsHeaderTitleLink) | |
this.$commentsHeader.appendChild(this.$commentsHeaderTitle) | |
this.$comments.appendChild(this.$commentsHeader) | |
this.$error = document.createElement('p') | |
this.$error.style.display = 'none' | |
this.$commentList = document.createElement('ul') | |
this.$comments.style.display = 'none' | |
this.$reset = document.createElement('a') | |
this.$reset.href = 'javascript:;' | |
this.$reset.className = `${this.namespace}-forget-link` | |
this.$reset.innerText = 'Forget me' | |
this.$reset.onclick = () => { | |
if (confirm('Are you sure you want to clear your GitHub Access Token?')) { | |
this.reset() | |
} | |
} | |
this.$commentForm = document.createElement('form') | |
const $commentFormTextarea = document.createElement('textarea') | |
const $commentFormButton = document.createElement('button') | |
$commentFormButton.innerText = 'Comment' | |
$commentFormButton.type = 'submit' | |
$commentFormTextarea.placeholder = 'Leave a comment' | |
this.$commentForm.onsubmit = async (event) => { | |
event.preventDefault(); | |
const comment = $commentFormTextarea.value | |
this.$commentForm.reset() | |
this.$commentForm.setAttribute('disabled', 'disabled') | |
await this.addComment(comment) | |
this.$commentForm.removeAttribute('disabled') | |
} | |
this.$comments.onMounted = () => { | |
$commentFormTextarea.focus() | |
this.fetch(); | |
} | |
this.$commentForm.appendChild($commentFormTextarea) | |
this.$commentForm.appendChild($commentFormButton) | |
this.$comments.appendChild(this.$error) | |
this.$commentsHeaderTitle.appendChild(this.$reset) | |
this.$comments.appendChild(this.$commentList) | |
this.$commenter.appendChild(this.$comments) | |
this.$comments.appendChild(this.$commentForm) | |
$target.appendChild(this.$commenter) | |
$target.appendChild($style) | |
} | |
updateAccessToken(accessToken) { | |
this.accessToken = accessToken | |
localStorage.setItem(this.cookieName, accessToken) | |
} | |
updateCounter(content) { | |
this.$counter.innerText = content | |
} | |
async _fetchIssue(url) { | |
const q = encodeURIComponent(`type:issue in:title repo:${this.repo} ${url}`) | |
const response = await fetch(`https://api.github.com/search/issues?q=${q}&sort=created&order=asc`, { | |
method: 'get', | |
headers: { | |
Accept: 'application/vnd.github.v3+json', | |
'Content-Type': 'application/json', | |
Authorization: `token ${this.accessToken}` | |
} | |
}) | |
const data = await response.json() | |
if (data.message) { | |
this.error(data.message) | |
return null | |
} | |
return data.items.filter((item) => item.title === url)[0] | |
} | |
async _fetchComments(issue) { | |
const response = await fetch(issue.comments_url, { | |
method: 'get', | |
headers: { | |
Accept: 'application/vnd.github.v3+json', | |
'Content-Type': 'application/json', | |
Authorization: `token ${this.accessToken}` | |
} | |
}) | |
const data = await response.json() | |
if (data.message) { | |
this.error(data.message) | |
return [] | |
} | |
return data | |
} | |
async _createIssue(url) { | |
const response = await fetch(`https://api.github.com/repos/${this.repo}/issues`, { | |
method: 'post', | |
headers: { | |
Accept: 'application/vnd.github.v3+json', | |
'Content-Type': 'application/json', | |
Authorization: `token ${this.accessToken}` | |
}, | |
body: JSON.stringify({ | |
title: url | |
}) | |
}) | |
const data = await response.json() | |
if (data.message) { | |
alert(data.message) | |
return | |
} | |
return data | |
} | |
async _createComment(issue, comment) { | |
const response = await fetch(issue.comments_url, { | |
method: 'post', | |
headers: { | |
Accept: 'application/vnd.github.v3+json', | |
'Content-Type': 'application/json', | |
Authorization: `token ${this.accessToken}` | |
}, | |
body: JSON.stringify({ | |
body: comment | |
}) | |
}) | |
const data = await response.json() | |
if (data.message) { | |
alert(data.message) | |
return | |
} | |
} | |
async addComment(comment) { | |
const url = window.location.href | |
let issue = await this._fetchIssue(url) | |
if (!issue) { | |
issue = await this._createIssue(url) | |
} | |
if (!issue) { | |
alert('Could not create the issue. :(') | |
return | |
} | |
this.updateHeader(issue) | |
await this._createComment(issue, comment) | |
await this._updateComments(issue) | |
} | |
async fetch() { | |
this.$commentList.scrollTop = this.$commentList.scrollHeight; | |
const issue = await this._fetchIssue(window.location.href) | |
if (!issue) { | |
this.updateCounter(0) | |
return | |
} | |
this.updateHeader(issue) | |
this._updateComments(issue) | |
} | |
async _updateComments(issue) { | |
const comments = await this._fetchComments(issue) | |
this.updateCounter(comments.length) | |
this.$commentList.innerHTML = '' | |
comments.forEach((comment) => { | |
const $comment = document.createElement('li') | |
const $body = document.createElement('div') | |
$body.innerHTML = this.linkify(comment.body) | |
$comment.appendChild($body) | |
const $author = document.createElement('p') | |
$author.className = `${this.namespace}-author` | |
$author.innerText = comment.user.login | |
$comment.appendChild($author) | |
this.$commentList.appendChild($comment) | |
}) | |
this.$commentList.scrollTop = this.$commentList.scrollHeight; | |
} | |
updateHeader(issue) { | |
this.$commentsHeaderTitleLink.style.display = 'inline-block' | |
this.$commentsHeaderTitleLink.href = issue.html_url | |
} | |
toggle() { | |
if (this.isOpen()) { | |
this.close() | |
} else if (this.accessToken) { | |
this.go('$comments') | |
} else { | |
this.go('$login') | |
} | |
} | |
error(msg) { | |
this.updateCounter('?') | |
this.$error.innerText = msg | |
this.$error.style.display = 'block' | |
} | |
isOpen() { | |
return this.$login.style.display !== 'none' || this.$comments.style.display !== 'none' | |
} | |
close() { | |
this.$overlay.style.display = 'none' | |
this.$login.style.display = 'none' | |
this.$comments.style.display = 'none' | |
} | |
linkify(str) { | |
if (!str) { | |
return ''; | |
} | |
return str.replace(Commenter.LINK_DETECTOR, (url) => { | |
let hyperlink = url | |
if (!hyperlink.match('^https?:\/\/')) { | |
hyperlink = `http://${hyperlink}` | |
} | |
return `<a href="${hyperlink}" target="_blank">${url}</a>` | |
}) | |
} | |
go(path) { | |
this.close() | |
this[path].style.display = 'block' | |
this.$overlay.style.display = 'block' | |
if (typeof this[path].onMounted === 'function') { | |
this[path].onMounted() | |
} | |
} | |
reset() { | |
this.$error.style.display = 'none' | |
this.$error.innerHTML = '' | |
this.$commentList.innerHTML = '' | |
this.updateCounter('?') | |
this.accessToken = null | |
localStorage.removeItem(this.cookieName) | |
this.close() | |
} | |
} | |
Commenter.ICON = (color) => { | |
return `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path fill="${color}" d="M 2 5 L 2 21 L 6 21 L 6 26.09375 L 7.625 24.78125 L 12.34375 21 L 22 21 L 22 5 Z M 4 7 L 20 7 L 20 19 L 11.65625 19 L 11.375 19.21875 L 8 21.90625 L 8 19 L 4 19 Z M 24 9 L 24 11 L 28 11 L 28 23 L 24 23 L 24 25.90625 L 20.34375 23 L 12.84375 23 L 10.34375 25 L 19.65625 25 L 26 30.09375 L 26 25 L 30 25 L 30 9 Z"/></svg>` | |
} | |
Commenter.LINK_DETECTOR = /(((https?:\/\/)|(www\.))[^\s]+)/g | |
;(() => { | |
new Commenter(document.body, 'commenter', 'dnsimple/dnsimple-admin-comments') | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment