GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

当前为 2016-05-24 提交的版本,查看 最新版本

  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://greasyfork.org/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. })();