Greasy Fork is available in English.

GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

Fra 24.05.2016. Se den seneste versjonen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name GitHub Toggle Issue Comments
  3. // @version 1.0.8
  4. // @description A userscript that toggles issues/pull request comments & messages
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace http://github.com/Mottie
  7. // @include https://github.com/*
  8. // @run-at document-idle
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @author Rob Garrison
  13. // ==/UserScript==
  14. /* global GM_addStyle, GM_getValue, GM_setValue */
  15. /*jshint unused:true */
  16. (function() {
  17. "use strict";
  18.  
  19. GM_addStyle([
  20. ".ghic-button { float:right; }",
  21. ".ghic-button .btn:hover div.select-menu-modal-holder { display:block; top:auto; bottom:25px; right:0; }",
  22. ".ghic-right { float:right; }",
  23. // pre-wrap set for Firefox; see https://greatest.deepsurf.us/en/forum/discussion/9166/x
  24. ".ghic-menu label { display:block; padding:5px 15px; white-space:pre-wrap; }",
  25. ".ghic-button .select-menu-header, .ghic-participants { cursor:default; }",
  26. ".ghic-participants { border-top:1px solid #484848; padding:15px; }",
  27. ".ghic-avatar { display:inline-block; float:left; margin: 0 2px 2px 0; cursor:pointer; position:relative; }",
  28. ".ghic-avatar:last-child { margin-bottom:5px; }",
  29. ".ghic-avatar.comments-hidden svg { display:block; position:absolute; top:-2px; left:-2px; z-index:1; }",
  30. ".ghic-avatar.comments-hidden img { opacity:0.5; }",
  31. ".ghic-button .dropdown-item input:checked ~ svg,",
  32. ".ghic-button .dropdown-item input:checked ~ .ghic-count { display:inline-block; }",
  33. ".ghic-button .ghic-count { float:left; margin-right:5px; }",
  34. ".ghic-button .select-menu-modal { margin:0; }",
  35. ".ghic-button .ghic-participants { margin-bottom:20px; }",
  36. // for testing: ".ghic-hidden { opacity: 0.3; }",
  37. ".ghic-hidden, .ghic-hidden-participant, .ghic-avatar svg, .ghic-button .ghic-right > *,",
  38. ".ghic-hideReactions .comment-reactions { display:none; }",
  39. ].join(""));
  40.  
  41. var busy = false,
  42.  
  43. // ZenHub addon active (include ZenHub Enterprise)
  44. hasZenHub = document.querySelector(".zhio, zhe") ? true : false,
  45.  
  46. iconHidden = "<svg class='octicon' xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 9 9'><path fill='#777' d='M7.07 4.5c0-.47-.12-.9-.35-1.3L3.2 6.7c.4.25.84.37 1.3.37.35 0 .68-.07 1-.2.32-.14.6-.32.82-.55.23-.23.4-.5.55-.82.13-.32.2-.65.2-1zM2.3 5.8l3.5-3.52c-.4-.23-.83-.35-1.3-.35-.35 0-.68.07-1 .2-.3.14-.6.32-.82.55-.23.23-.4.5-.55.82-.13.32-.2.65-.2 1 0 .47.12.9.36 1.3zm6.06-1.3c0 .7-.17 1.34-.52 1.94-.34.6-.8 1.05-1.4 1.4-.6.34-1.24.52-1.94.52s-1.34-.18-1.94-.52c-.6-.35-1.05-.8-1.4-1.4C.82 5.84.64 5.2.64 4.5s.18-1.35.52-1.94.8-1.06 1.4-1.4S3.8.64 4.5.64s1.35.17 1.94.52 1.06.8 1.4 1.4c.35.6.52 1.24.52 1.94z'/></svg>",
  47. iconCheck = "<svg class='octicon octicon-check' height='16' viewBox='0 0 12 16' width='12'><path d='M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z'></path></svg>",
  48. plus1Icon = "<img src='https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png' class='emoji' title=':+1:' alt=':+1:' height='20' width='20' align='absmiddle'>",
  49.  
  50. settings = {
  51. // https://github.com/Mottie/Keyboard/issues/448
  52. title: {
  53. isHidden: false,
  54. name: "ghic-title",
  55. selector: ".discussion-item-renamed",
  56. label: "Title Changes"
  57. },
  58. labels: {
  59. isHidden: false,
  60. name: "ghic-labels",
  61. selector: ".discussion-item-labeled, .discussion-item-unlabeled",
  62. label: "Label Changes"
  63. },
  64. state: {
  65. isHidden: false,
  66. name: "ghic-state",
  67. selector: ".discussion-item-reopened, .discussion-item-closed",
  68. label: "State Changes (close/reopen)"
  69. },
  70.  
  71. // https://github.com/jquery/jquery/issues/2986
  72. milestone: {
  73. isHidden: false,
  74. name: "ghic-milestone",
  75. selector: ".discussion-item-milestoned",
  76. label: "Milestone Changes"
  77. },
  78. refs: {
  79. isHidden: false,
  80. name: "ghic-refs",
  81. selector: ".discussion-item-ref, .discussion-item-head_ref_deleted",
  82. label: "References"
  83. },
  84. assigned: {
  85. isHidden: false,
  86. name: "ghic-assigned",
  87. selector: ".discussion-item-assigned",
  88. label: "Assignment Changes"
  89. },
  90.  
  91. // Pull Requests
  92. commits: {
  93. isHidden: false,
  94. name: "ghic-commits",
  95. selector: ".discussion-commits",
  96. label: "Commits"
  97. },
  98. // https://github.com/jquery/jquery/pull/3014
  99. diffOld: {
  100. isHidden: false,
  101. name: "ghic-diffOld",
  102. selector: ".outdated-diff-comment-container",
  103. label: "Diff (outdated) Comments"
  104. },
  105. diffNew: {
  106. isHidden: false,
  107. name: "ghic-diffNew",
  108. selector: "[id^=diff-for-comment-]:not(.outdated-diff-comment-container)",
  109. label: "Diff (current) Comments"
  110. },
  111. // https://github.com/jquery/jquery/pull/2949
  112. merged: {
  113. isHidden: false,
  114. name: "ghic-merged",
  115. selector: ".discussion-item-merged",
  116. label: "Merged"
  117. },
  118. integrate: {
  119. isHidden: false,
  120. name: "ghic-integrate",
  121. selector: ".discussion-item-integrations-callout",
  122. label: "Integrations"
  123. },
  124.  
  125. // extras (special treatment - no selector)
  126. plus1: {
  127. isHidden: false,
  128. name: "ghic-plus1",
  129. label: "Hide +1s"
  130. },
  131. reactions: {
  132. isHidden: false,
  133. name: "ghic-reactions",
  134. label: "Reactions"
  135. },
  136. // page with lots of users to hide:
  137. // https://github.com/isaacs/github/issues/215
  138.  
  139. // ZenHub pipeline change
  140. pipeline: {
  141. isHidden: false,
  142. name: "ghic-pipeline",
  143. selector: ".discussion-item.zh-discussion-item",
  144. label: "ZenHub Pipeline Changes"
  145. }
  146. },
  147.  
  148. matches = function(el, selector) {
  149. // https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
  150. var matches = document.querySelectorAll(selector),
  151. i = matches.length;
  152. while (--i >= 0 && matches.item(i) !== el) {}
  153. return i > -1;
  154. },
  155.  
  156. closest = function(el, selector) {
  157. while (el && !matches(el, selector)) {
  158. el = el.parentNode;
  159. }
  160. return matches(el, selector) ? el : null;
  161. },
  162.  
  163. addClass = function(els, name) {
  164. var indx,
  165. len = els.length;
  166. for (indx = 0; indx < len; indx++) {
  167. els[indx].classList.add(name);
  168. }
  169. return len;
  170. },
  171.  
  172. removeClass = function(els, name) {
  173. var indx,
  174. len = els.length;
  175. for (indx = 0; indx < len; indx++) {
  176. els[indx].classList.remove(name);
  177. }
  178. },
  179.  
  180. addMenu = function() {
  181. busy = true;
  182. if (document.getElementById("discussion_bucket") && !document.querySelector(".ghic-button")) {
  183. // update "isHidden" values
  184. getSettings();
  185. var name,
  186. list = "",
  187. header = document.querySelector(".discussion-sidebar-item:last-child"),
  188. menu = document.createElement("div");
  189.  
  190. for (name in settings) {
  191. if (settings.hasOwnProperty(name) && !(name === "pipeline" && !hasZenHub)) {
  192. list += "<label class='dropdown-item'>" + settings[name].label +
  193. "<span class='ghic-right " + settings[name].name + "'>" +
  194. "<input type='checkbox'" + (settings[name].isHidden ? " checked" : "") + ">" +
  195. iconCheck + "<span class='ghic-count'> </span></span></label>";
  196. }
  197. }
  198.  
  199. menu.className = "ghic-button";
  200. menu.innerHTML = [
  201. "<span class='btn btn-sm' role='button' tabindex='0' aria-haspopup='true'>",
  202. "<span class='tooltipped tooltipped-w' aria-label='Toggle issue comments'>",
  203. "<svg class='octicon octicon-comment-discussion' height='16' width='16' role='img' viewBox='0 0 16 16'><path d='M15 2H6c-0.55 0-1 0.45-1 1v2H1c-0.55 0-1 0.45-1 1v6c0 0.55 0.45 1 1 1h1v3l3-3h4c0.55 0 1-0.45 1-1V10h1l3 3V10h1c0.55 0 1-0.45 1-1V3c0-0.55-0.45-1-1-1zM9 12H4.5l-1.5 1.5v-1.5H1V6h4v3c0 0.55 0.45 1 1 1h3v2z m6-3H13v1.5l-1.5-1.5H6V3h9v6z'></path></svg>",
  204. "</span>",
  205. "<div class='select-menu-modal-holder'>",
  206. "<div class='select-menu-modal' aria-hidden='true'>",
  207. "<div class='select-menu-header' tabindex='-1'>",
  208. "<span class='select-menu-title'>Toggle items</span>",
  209. "</div>",
  210. "<div class='select-menu-list ghic-menu' role='menu'>",
  211. list,
  212. "<div class='ghic-participants'></div>",
  213. "</div>",
  214. "</div>",
  215. "</div>",
  216. "</span>"
  217. ].join("");
  218. if (hasZenHub) {
  219. header.insertBefore(menu, header.childNodes[0]);
  220. } else {
  221. header.appendChild(menu);
  222. }
  223. addAvatars();
  224. }
  225. update();
  226. busy = false;
  227. },
  228.  
  229. addAvatars = function() {
  230. var indx = 0,
  231.  
  232. str = "<h3>Hide Comments from</h3>",
  233. unique = [],
  234. // get all avatars
  235. avatars = document.querySelectorAll(".timeline-comment-avatar"),
  236. len = avatars.length - 1, // last avatar is the new comment with the current user
  237.  
  238. loop = function(callback) {
  239. var el, name,
  240. max = 0;
  241. while (max < 50 && indx < len) {
  242. if (indx >= len) {
  243. return callback();
  244. }
  245. el = avatars[indx];
  246. name = (el.getAttribute("alt") || "").replace("@", "");
  247. if (unique.indexOf(name) < 0) {
  248. str += "<span class='ghic-avatar tooltipped tooltipped-n' aria-label='" + name + "'>" +
  249. iconHidden +
  250. "<img class='ghic-avatar avatar' width='24' height='24' src='" + el.src + "'/>" +
  251. "</span>";
  252. unique[unique.length] = name;
  253. max++;
  254. }
  255. indx++;
  256. }
  257. if (indx < len) {
  258. setTimeout(function() {
  259. loop(callback);
  260. }, 200);
  261. } else {
  262. callback();
  263. }
  264. };
  265. loop(function() {
  266. document.querySelector(".ghic-participants").innerHTML = str;
  267. });
  268. },
  269.  
  270. getSettings = function() {
  271. var name;
  272. for (name in settings) {
  273. if (settings.hasOwnProperty(name)) {
  274. settings[name].isHidden = GM_getValue(settings[name].name, false);
  275. }
  276. }
  277. },
  278.  
  279. saveSettings = function() {
  280. var name;
  281. for (name in settings) {
  282. if (settings.hasOwnProperty(name)) {
  283. GM_setValue(settings[name].name, settings[name].isHidden);
  284. }
  285. }
  286. },
  287.  
  288. getInputValues = function() {
  289. var name,
  290. menu = document.querySelector(".ghic-menu");
  291. for (name in settings) {
  292. if (settings.hasOwnProperty(name) && !(name === "pipeline" && !hasZenHub)) {
  293. settings[name].isHidden = menu.querySelector("." + settings[name].name + " input").checked;
  294. }
  295. }
  296. },
  297.  
  298. hideStuff = function(name, init) {
  299. if (settings[name].selector) {
  300. var count,
  301. results = document.querySelectorAll(settings[name].selector);
  302. if (settings[name].isHidden) {
  303. count = addClass(results, "ghic-hidden");
  304. document.querySelector(".ghic-menu ." + settings[name].name + " .ghic-count")
  305. .textContent = count ? "(" + count + ")" : " ";
  306. } else if (!init) {
  307. // no need to remove classes on initialization
  308. removeClass(results, "ghic-hidden");
  309. }
  310. } else if (name === "plus1") {
  311. hidePlus1(init);
  312. } else if (name === "reactions") {
  313. document.querySelector("body").classList[settings[name].isHidden ? "add" : "remove"]("ghic-hideReactions");
  314. }
  315. },
  316.  
  317. hidePlus1 = function(init) {
  318. if (init && !settings.plus1.isHidden) { return; }
  319. var max,
  320. indx = 0,
  321. count = 0,
  322. total = 0,
  323. // keep a list of post authors to prevent duplicate +1 counts
  324. authors = [],
  325. // used https://github.com/isaacs/github/issues/215 for matches here...
  326. // matches "+1!!!!", "++1", "+!", "+99!!!", "-1", "+ 100", "thumbs up"; ":+1:^21425235"
  327. // ignoring -1's...
  328. regexPlus = /([?!,.:^[\]()\'\"+-\d]|bump|thumbs|up)/gi,
  329. // other comments to hide - they are still counted towards the +1 counter (for now?)
  330. // seen "^^^" to bump posts; "bump plleeaaassee"; "eta?"; "pretty please"
  331. // "need this"; "right now"; "still nothing?"; "super helpful"; "for gods sake"
  332. regexHide = new RegExp("(" + [
  333. "@\\w+",
  334. "pretty",
  335. "pl+e+a+s+e+",
  336. "y+e+s+",
  337. "eta",
  338. "much",
  339. "need(ed)?",
  340. "fix",
  341. "this",
  342. "right",
  343. "now",
  344. "still",
  345. "nothing",
  346. "super",
  347. "helpful",
  348. "for\\sgods\\ssake"
  349. ].join("|") + ")", "gi"),
  350. // image title ":{anything}:", etc.
  351. regexEmoji = /:(.*):/,
  352.  
  353. comments = document.querySelectorAll(".js-discussion .timeline-comment-wrapper"),
  354. len = comments.length,
  355.  
  356. loop = function() {
  357. var wrapper, el, tmp, txt, img, hasLink, dupe;
  358. max = 0;
  359. while (max < 20 && indx < len) {
  360. if (indx >= len) {
  361. return;
  362. }
  363. wrapper = comments[indx];
  364. // save author list to prevent repeat +1s
  365. el = wrapper.querySelector(".timeline-comment-header .author");
  366. txt = (el ? el.textContent || "" : "").toLowerCase();
  367. dupe = true;
  368. if (txt && authors.indexOf(txt) < 0) {
  369. authors[authors.length] = txt;
  370. dupe = false;
  371. }
  372. el = wrapper.querySelector(".comment-body");
  373. // ignore quoted messages, but get all fragments
  374. tmp = el.querySelectorAll(".email-fragment");
  375. // some posts only contain a link to related issues; these should not be counted as a +1
  376. // see https://github.com/isaacs/github/issues/618#issuecomment-200869630
  377. hasLink = el.querySelectorAll(tmp.length ? ".email-fragment .issue-link" : ".issue-link").length;
  378. if (tmp.length) {
  379. // ignore quoted messages
  380. txt = getAllText(tmp);
  381. } else {
  382. txt = el.textContent.trim();
  383. }
  384. if (!txt) {
  385. img = el.querySelector("img");
  386. if (img) {
  387. txt = img.getAttribute("title") || img.getAttribute("alt");
  388. }
  389. }
  390. // remove fluff
  391. txt = txt.replace(regexEmoji, "").replace(regexPlus, "").replace(regexHide, "").trim();
  392. if (txt === "" || (txt.length < 4 && !hasLink)) {
  393. if (settings.plus1.isHidden) {
  394. wrapper.classList.add("ghic-hidden");
  395. total++;
  396. // one +1 per author
  397. if (!dupe) {
  398. count++;
  399. }
  400. } else if (!init) {
  401. wrapper.classList.remove("ghic-hidden");
  402. }
  403. max++;
  404. }
  405. indx++;
  406. }
  407. if (indx < len) {
  408. setTimeout(function() {
  409. loop();
  410. }, 200);
  411. } else {
  412. document.querySelector(".ghic-menu .ghic-plus1 .ghic-count")
  413. .textContent = total ? "(" + total + ")" : " ";
  414. addCountToReaction(count);
  415. }
  416. };
  417. loop();
  418. },
  419.  
  420. getAllText = function(el) {
  421. var txt = "",
  422. indx = el.length;
  423. // text order doesn't matter
  424. while (indx--) {
  425. txt += el[indx].textContent.trim();
  426. }
  427. return txt;
  428. },
  429.  
  430. addCountToReaction = function(count) {
  431. if (!count) {
  432. count = (document.querySelector(".ghic-menu .ghic-plus1 .ghic-count").textContent || "").trim();
  433. }
  434. var comment = document.querySelector(".timeline-comment"),
  435. tmp = comment.querySelector(".has-reactions button[value='+1 react'], .has-reactions button[value='+1 unreact']"),
  436. el = comment.querySelector(".ghic-count");
  437. if (el) {
  438. // the count may have been appended to the comment & now
  439. // there is a reaction, so remove any "ghic-count" elements
  440. el.parentNode.removeChild(el);
  441. }
  442. if (count) {
  443. if (tmp) {
  444. el = document.createElement("span");
  445. el.className = "ghic-count";
  446. el.textContent = count ? " + " + count + " (from hidden comments)" : "";
  447. tmp.appendChild(el);
  448. } else {
  449. el = document.createElement("p");
  450. el.className = "ghic-count";
  451. el.innerHTML = "<hr>" + plus1Icon + " " + count + " (from hidden comments)";
  452. comment.querySelector(".comment-body").appendChild(el);
  453. }
  454. }
  455. },
  456.  
  457. hideParticipant = function(el) {
  458. var els, indx, len, hide, name,
  459. results = [];
  460. if (el) {
  461. el.classList.toggle("comments-hidden");
  462. hide = el.classList.contains("comments-hidden");
  463. name = el.getAttribute("aria-label");
  464. els = document.querySelectorAll(".js-discussion .author");
  465. len = els.length;
  466. for (indx = 0; indx < len; indx++) {
  467. if (els[indx].textContent.trim() === name) {
  468. results[results.length] = closest(els[indx], ".timeline-comment-wrapper, .commit-comment, .discussion-item");
  469. }
  470. }
  471. // use a different participant class name to hide timeline events
  472. // or unselecting all users will show everything
  473. if (el.classList.contains("comments-hidden")) {
  474. addClass(results, "ghic-hidden-participant");
  475. } else {
  476. removeClass(results, "ghic-hidden-participant");
  477. }
  478. results = [];
  479. }
  480. },
  481.  
  482. regex = /(svg|path)/i,
  483.  
  484. update = function() {
  485. busy = true;
  486. if (document.querySelector("#discussion_bucket") && document.querySelector(".ghic-button")) {
  487. var keys = Object.keys(settings),
  488. indx = keys.length;
  489. while (indx--) {
  490. // true flag for init - no need to remove classes
  491. hideStuff(keys[indx], true);
  492. }
  493. }
  494. busy = false;
  495. },
  496.  
  497. checkItem = function(event) {
  498. busy = true;
  499. if (document.getElementById("discussion_bucket")) {
  500. var name,
  501. target = event.target,
  502. wrap = target && target.parentNode;
  503. if (target && wrap) {
  504. if (target.nodeName === "INPUT" && wrap.classList.contains("ghic-right")) {
  505. getInputValues();
  506. saveSettings();
  507. // extract ghic-{name}, because it matches the name in settings
  508. name = wrap.className.replace("ghic-right", "").trim();
  509. if (wrap.classList.contains(name)) {
  510. hideStuff(name.replace("ghic-", ""));
  511. }
  512. } else if (target.classList.contains("ghic-avatar")) {
  513. // make sure we're targeting the span wrapping the image
  514. hideParticipant(target.nodeName === "IMG" ? target.parentNode : target);
  515. } else if (regex.test(target.nodeName)) {
  516. // clicking on the SVG may target the svg or path inside
  517. hideParticipant(closest(target, ".ghic-avatar"));
  518. }
  519. }
  520. }
  521. busy = false;
  522. },
  523.  
  524. init = function() {
  525. busy = true;
  526. getSettings();
  527. addMenu();
  528. document.querySelector("body").addEventListener("input", checkItem);
  529. document.querySelector("body").addEventListener("click", checkItem);
  530. update();
  531. busy = false;
  532. },
  533.  
  534. // DOM targets - to detect GitHub dynamic ajax page loading
  535. targets = document.querySelectorAll([
  536. "#js-repo-pjax-container",
  537. "#js-pjax-container",
  538. ".js-discussion"
  539. ].join(","));
  540.  
  541. // update TOC when content changes
  542. Array.prototype.forEach.call(targets, function(target) {
  543. new MutationObserver(function(mutations) {
  544. mutations.forEach(function(mutation) {
  545. // preform checks before adding code wrap to minimize function calls
  546. if (!busy && mutation.target === target) {
  547. addMenu();
  548. }
  549. });
  550. }).observe(target, {
  551. childList: true,
  552. subtree: true
  553. });
  554. });
  555.  
  556. init();
  557.  
  558. })();