diff --git a/github-toggle-diff-comments.user.js b/github-toggle-diff-comments.user.js
new file mode 100644
index 0000000..9a9d3b6
--- /dev/null
+++ b/github-toggle-diff-comments.user.js
@@ -0,0 +1,257 @@
+// ==UserScript==
+// @name GitHub Toggle Diff Comments
+// @version 0.1.0
+// @description A userscript that toggles diff/PR comments
+// @license MIT
+// @author Rob Garrison
+// @namespace https://github.com/Mottie
+// @include https://github.com/*
+// @run-at document-idle
+// @grant none
+// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
+// @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=198500
+// @icon https://assets-cdn.github.com/pinned-octocat.svg
+// @updateURL https://raw.githubusercontent.com/Mottie/Github-userscripts/master/github-toggle-diff-comments.user.js
+// @downloadURL https://raw.githubusercontent.com/Mottie/Github-userscripts/master/github-toggle-diff-comments.user.js
+// ==/UserScript==
+(() => {
+ "use strict";
+
+ let timer,
+ ignoreEvents = false;
+ const targets = {
+ // show comments wrapper for each file
+ headerComment: "show-file-notes",
+ // show comments checkbox
+ headerCheckbox: "js-toggle-file-notes",
+ // comment block row - class added to TR containing the comment
+ rowComment: "inline-comments"
+ },
+ icons = {
+ "show": `
+ `,
+ "collapse": ``
+ },
+ activeClass = "ghtc-active",
+ button = document.createElement("div");
+ button.className = "btn btn-sm BtnGroup-item ghtc-toggle";
+
+ // Using small black triangles because Windows doesn't
+ // replace them with ugly emoji images
+ GM.addStyle(`
+ td.js-quote-selection-container {
+ position: relative;
+ }
+ .review-thread:before {
+ content: "\\25be";
+ font-size: 2rem;
+ position: absolute;
+ right: 10px;
+ top: -1rem;
+ pointer-events: none;
+ }
+ .ghtc-collapsed .review-thread:before {
+ content: "\\25c2";
+ }
+ .ghtc-collapsed .review-thread {
+ padding: 0 0 5px;
+ border: 0;
+ }
+ .ghtc-toggle .ghtc-secondary,
+ .ghtc-toggle.${activeClass} .ghtc-primary,
+ .ghtc-toggle input ~ .ghtc-secondary,
+ .ghtc-toggle input:checked ~ .ghtc-primary,
+ .ghtc-collapsed .review-thread > *,
+ .ghtc-collapsed .last-review-thread,
+ .ghtc-collapsed .inline-comment-form-container {
+ display: none;
+ }
+ .ghtc-collapsed td.line-comments {
+ padding: 0 5px;
+ cursor: pointer;
+ }
+ .pr-toolbar .pr-review-tools.float-right .diffbar-item + .diffbar-item {
+ margin-left: 10px;
+ }
+ .ghtc-toggle {
+ height: 28px;
+ }
+ .ghtc-toggle svg {
+ display: inline-block;
+ max-height: 16px;
+ pointer-events: none;
+ vertical-align: baseline !important;
+ }
+ .ghtc-toggle.${activeClass} .ghtc-secondary,
+ .ghtc-toggle input:checked ~ .ghtc-secondary {
+ display: block;
+ }`
+ );
+
+ function toggleSingleComment(el) {
+ // Toggle individual inline comment
+ el.parentNode.classList.toggle("ghtc-collapsed");
+ }
+
+ function toggleMultipleComments(wrapper, state) {
+ $(".ghtc-collapse-toggle-file", wrapper).classList.toggle(activeClass, state);
+ $$(`tr.${targets.rowComment}`, wrapper).forEach(el => {
+ el.classList.toggle("ghtc-collapsed", state);
+ });
+ }
+
+ function getState(el) {
+ el.classList.toggle(activeClass);
+ return el.classList.contains(activeClass);
+ }
+
+ function toggleFile(el) {
+ // Toggle all inline comments for one file
+ const state = getState(el);
+ toggleMultipleComments(el.closest(".file"), state);
+ }
+
+ function toggleAll(el) {
+ // Toggle all comments on page
+ const state = getState(el);
+ $("#ghtc-collapse-toggle-all").classList.toggle(activeClass, state);
+ toggleMultipleComments(el.closest("#files_bucket"), state);
+ $$(".ghtc-collapse-toggle-file").forEach(el => {
+ el.classList.toggle(activeClass, state);
+ });
+ }
+
+ function showAll(el) {
+ // Show/hide all comments on page
+ const state = getState(el);
+ $("#ghtc-show-toggle-all").classList.toggle(activeClass, state);
+ $$("#files_bucket .js-toggle-file-notes").forEach(el => {
+ el.checked = state;
+ el.dispatchEvent(new Event("change", {bubbles: true}));
+ });
+ }
+
+ function createButton({id, className, icon, title}) {
+ const btn = button.cloneNode(true);
+ if (id) {
+ btn.id = id;
+ }
+ btn.className += ` ${className || ""}`;
+ btn.title = title;
+ btn.innerHTML = icons[icon];
+ return btn;
+ }
+
+ function execFunction(event, callback) {
+ clearTimeout(timer);
+ ignoreEvents = true;
+ event.stopPropagation();
+ event.preventDefault();
+ callback(event.target);
+ timer = setTimeout(() => {
+ ignoreEvents = false;
+ }, 250);
+ }
+
+ function addListeners() {
+ $("#files_bucket").addEventListener("change", event => {
+ const el = event.target;
+ if (el && el.classList.contains(targets.headerCheckbox)) {
+ el.parentNode.classList.toggle(activeClass, el.checked);
+ }
+ });
+ $("#files_bucket").addEventListener("click", event => {
+ const el = event.target;
+ if (!ignoreEvents && el) {
+ const shift = event.shiftKey,
+ toggle = el.classList.contains("ghtc-collapse-toggle-file"),
+ show = el.nodeName === "LABEL",
+ comment = el.classList.contains("js-quote-selection-container");
+ if (el.id === "ghtc-collapse-toggle-all" || toggle && shift) {
+ execFunction(event, toggleAll);
+ } else if (el.id === "ghtc-show-toggle-all" || show && shift) {
+ execFunction(event, showAll);
+ } else if (toggle || comment && shift) {
+ execFunction(event, toggleFile);
+ } else if (comment) {
+ execFunction(event, toggleSingleComment);
+ }
+ }
+ });
+ }
+
+ function addButtons() {
+ $$(`.${targets.headerComment}`).forEach(wrapper => {
+ if (!wrapper.classList.contains("ghtc-hidden")) {
+ const label = $("label", wrapper),
+ checkbox = $("input", wrapper);
+ let btn;
+ // Make span wrapper a button group
+ wrapper.classList.add("ghtc-hidden", "BtnGroup");
+ // Remove top margin
+ wrapper.classList.remove("pt-1");
+
+ // Convert "Show Comments" label wrapping checkbox into a button
+ label.className = "btn btn-sm BtnGroup-item ghtc-toggle";
+ label.title = "Show or hide all comments in this file";
+ label.innerHTML = `
+
+ ${icons.show}`;
+
+ // Add collapse all file comments button before label
+ btn = createButton({
+ className: "ghtc-collapse-toggle-file",
+ icon: "collapse",
+ title: "Expand or collapse all comments in this file"
+ });
+ label.parentNode.insertBefore(btn, label);
+ // Hide checkbox
+ checkbox.setAttribute("hidden", true);
+ }
+ });
+ // Add collapse all comments on the page
+ if (!$("#ghtc-collapse-toggle-all")) {
+ const wrapper = document.createElement("div"),
+ // insert before Unified/Split button group
+ diffmode = $(".pr-review-tools .diffbar-item");
+ let btn;
+ wrapper.className = "BtnGroup diffbar-item";
+ diffmode.parentNode.insertBefore(wrapper, diffmode);
+ // collapse/expand all comments
+ btn = createButton({
+ id: "ghtc-collapse-toggle-all",
+ icon: "collapse",
+ title: "Expand or collapse all comments"
+ });
+ wrapper.appendChild(btn);
+ // show/hide all comments
+ btn = createButton({
+ id: "ghtc-show-toggle-all",
+ icon: "show",
+ className: activeClass,
+ title: "Show or hide all comments"
+ });
+ wrapper.appendChild(btn);
+ }
+ }
+
+ function $(str, el) {
+ return (el || document).querySelector(str);
+ }
+
+ function $$(str, el) {
+ return [...(el || document).querySelectorAll(str)];
+ }
+
+ function init() {
+ if ($("#files_bucket") && $(".pr-toolbar")) {
+ addButtons();
+ addListeners();
+ }
+ }
+
+ document.addEventListener("ghmo:container", init);
+ document.addEventListener("ghmo:diff", init);
+ init();
+
+})();
diff --git a/images/github-toggle-diff-comments.gif b/images/github-toggle-diff-comments.gif
new file mode 100644
index 0000000..e5661c0
Binary files /dev/null and b/images/github-toggle-diff-comments.gif differ