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 hidden or 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