GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

当前为 2016-07-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Toggle Issue Comments
  3. // @version 1.0.17
  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, esnext: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 span { font-weight:normal; opacity:.5; }
  32. .ghic-button .dropdown-item.ghic-has-content span { opacity:1; }
  33. .ghic-button .dropdown-item.ghic-checked span { font-weight:bold; }
  34. .ghic-button .dropdown-item.ghic-checked svg,
  35. .ghic-button .dropdown-item.ghic-checked .ghic-count { display:inline-block; }
  36. .ghic-button .ghic-count { float:left; margin-right:5px; }
  37. .ghic-button .select-menu-modal { margin:0; }
  38. .ghic-button .ghic-participants { margin-bottom:20px; }
  39. /* for testing: ".ghic-hidden { opacity: 0.3; } */
  40. .ghic-hidden, .ghic-hidden-participant, .ghic-avatar svg, .ghic-button .ghic-right > *,
  41. .ghic-hideReactions .comment-reactions { display:none; }
  42. `);
  43.  
  44. let targets,
  45. busy = false,
  46. // ZenHub addon active (include ZenHub Enterprise)
  47. hasZenHub = $(".zhio, .zhe") ? true : false;
  48.  
  49. const regex = /(svg|path)/i,
  50.  
  51. settings = {
  52. // example: https://github.com/Mottie/Keyboard/issues/448
  53. title: {
  54. isHidden: false,
  55. name: "ghic-title",
  56. selector: ".discussion-item-renamed",
  57. label: "Title Changes"
  58. },
  59. labels: {
  60. isHidden: false,
  61. name: "ghic-labels",
  62. selector: ".discussion-item-labeled, .discussion-item-unlabeled",
  63. label: "Label Changes"
  64. },
  65. state: {
  66. isHidden: false,
  67. name: "ghic-state",
  68. selector: ".discussion-item-reopened, .discussion-item-closed",
  69. label: "State Changes (close/reopen)"
  70. },
  71.  
  72. // example: https://github.com/jquery/jquery/issues/2986
  73. milestone: {
  74. isHidden: false,
  75. name: "ghic-milestone",
  76. selector: ".discussion-item-milestoned",
  77. label: "Milestone Changes"
  78. },
  79. refs: {
  80. isHidden: false,
  81. name: "ghic-refs",
  82. selector: ".discussion-item-ref, .discussion-item-head_ref_deleted",
  83. label: "References"
  84. },
  85. assigned: {
  86. isHidden: false,
  87. name: "ghic-assigned",
  88. selector: ".discussion-item-assigned",
  89. label: "Assignment Changes"
  90. },
  91.  
  92. // Pull Requests
  93. commits: {
  94. isHidden: false,
  95. name: "ghic-commits",
  96. selector: ".discussion-commits",
  97. label: "Commits"
  98. },
  99. // example: https://github.com/jquery/jquery/pull/3014
  100. diffOld: {
  101. isHidden: false,
  102. name: "ghic-diffOld",
  103. selector: ".outdated-diff-comment-container",
  104. label: "Diff (outdated) Comments"
  105. },
  106. diffNew: {
  107. isHidden: false,
  108. name: "ghic-diffNew",
  109. selector: "[id^=diff-for-comment-]:not(.outdated-diff-comment-container)",
  110. label: "Diff (current) Comments"
  111. },
  112. // example: https://github.com/jquery/jquery/pull/2949
  113. merged: {
  114. isHidden: false,
  115. name: "ghic-merged",
  116. selector: ".discussion-item-merged",
  117. label: "Merged"
  118. },
  119. integrate: {
  120. isHidden: false,
  121. name: "ghic-integrate",
  122. selector: ".discussion-item-integrations-callout",
  123. label: "Integrations"
  124. },
  125.  
  126. // extras (special treatment - no selector)
  127. plus1: {
  128. isHidden: false,
  129. name: "ghic-plus1",
  130. label: "Hide +1s"
  131. },
  132. reactions: {
  133. isHidden: false,
  134. name: "ghic-reactions",
  135. label: "Reactions"
  136. },
  137. // page with lots of users to hide:
  138. // https://github.com/isaacs/github/issues/215
  139.  
  140. // ZenHub pipeline change
  141. pipeline: {
  142. isHidden: false,
  143. name: "ghic-pipeline",
  144. selector: ".discussion-item.zh-discussion-item",
  145. label: "ZenHub Pipeline Changes"
  146. }
  147. };
  148.  
  149. const 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>`,
  150. 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>`,
  151. 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">`;
  152.  
  153. function $(selector, el) {
  154. return (el || document).querySelector(selector);
  155. }
  156. function $$(selector, el) {
  157. return Array.from((el || document).querySelectorAll(selector));
  158. }
  159. function closest(el, selector) {
  160. while (el && el.nodeName !== "BODY" && !el.matches(selector)) {
  161. el = el.parentNode;
  162. }
  163. return el && el.matches(selector) ? el : null;
  164. }
  165. function addClass(els, name) {
  166. let indx,
  167. len = els.length;
  168. for (indx = 0; indx < len; indx++) {
  169. els[indx].classList.add(name);
  170. }
  171. return len;
  172. }
  173. function removeClass(els, name) {
  174. let indx,
  175. len = els.length;
  176. for (indx = 0; indx < len; indx++) {
  177. els[indx].classList.remove(name);
  178. }
  179. }
  180. function toggleClass(els, name, flag) {
  181. els = Array.isArray(els) ? els : [els];
  182. let el,
  183. indx = els.length;
  184. while (indx--) {
  185. el = els[indx];
  186. if (el) {
  187. if (typeof flag === "undefined") {
  188. flag = !el.classList.contains(name);
  189. }
  190. if (flag) {
  191. el.classList.add(name);
  192. } else {
  193. el.classList.remove(name);
  194. }
  195. }
  196. }
  197. }
  198.  
  199. function addMenu() {
  200. busy = true;
  201. if ($("#discussion_bucket") && !$(".ghic-button")) {
  202. // update "isHidden" values
  203. getSettings();
  204. let name, bright, isHidden, isChecked,
  205. list = "",
  206. keys = Object.keys(settings),
  207. header = $(".discussion-sidebar-item:last-child"),
  208. menu = document.createElement("div");
  209.  
  210. for (name of keys) {
  211. if (!(name === "pipeline" && !hasZenHub)) {
  212. // make plus1 and reactions list items always bright
  213. bright = name === "plus1" ? " ghic-has-content" : "";
  214. isHidden = settings[name].isHidden;
  215. isChecked = isHidden ? " ghic-checked": "";
  216. // not using multi-line backticks because it adds lots of white-space to the label
  217. list += `<label class="dropdown-item${bright}${isChecked}">` +
  218. `<span>${settings[name].label}</span>` +
  219. `<span class="ghic-right ${settings[name].name}">` +
  220. `<input type="checkbox"${isHidden ? " checked" : ""}>` +
  221. `${iconCheck}<span class="ghic-count"> </span>` +
  222. `</span></label>`;
  223. }
  224. }
  225.  
  226. menu.className = "ghic-button";
  227. menu.innerHTML = `
  228. <span class="btn btn-sm" role="button" tabindex="0" aria-haspopup="true">
  229. <span class="tooltipped tooltipped-w" aria-label="Toggle issue comments">
  230. <svg class="octicon octicon-comment-discussion" height="16" width="16" role="img" viewBox="0 0 16 16">
  231. <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>
  232. </svg>
  233. </span>
  234. <div class="select-menu-modal-holder">
  235. <div class="select-menu-modal" aria-hidden="true">
  236. <div class="select-menu-header" tabindex="-1">
  237. <span class="select-menu-title">Toggle items</span>
  238. </div>
  239. <div class="select-menu-list ghic-menu" role="menu">
  240. ${list}
  241. <div class="ghic-participants"></div>
  242. </div>
  243. </div>
  244. </div>
  245. </span>
  246. `;
  247. if (hasZenHub) {
  248. header.insertBefore(menu, header.childNodes[0]);
  249. } else {
  250. header.appendChild(menu);
  251. }
  252. addAvatars();
  253. }
  254. update();
  255. busy = false;
  256. }
  257.  
  258. function addAvatars() {
  259. let indx = 0,
  260.  
  261. str = "<h3>Hide Comments from</h3>",
  262. unique = [],
  263. // get all avatars
  264. avatars = $$(".timeline-comment-avatar"),
  265. len = avatars.length - 1, // last avatar is the new comment with the current user
  266.  
  267. loop = function(callback) {
  268. let el, name,
  269. max = 0;
  270. while (max < 50 && indx < len) {
  271. if (indx >= len) {
  272. return callback();
  273. }
  274. el = avatars[indx];
  275. name = (el.getAttribute("alt") || "").replace("@", "");
  276. if (unique.indexOf(name) < 0) {
  277. str += `<span class="ghic-avatar tooltipped tooltipped-n" aria-label="${name}">
  278. ${iconHidden}
  279. <img class="ghic-avatar avatar" width="24" height="24" src="${el.src}"/>
  280. </span>`;
  281. unique[unique.length] = name;
  282. max++;
  283. }
  284. indx++;
  285. }
  286. if (indx < len) {
  287. setTimeout(function() {
  288. loop(callback);
  289. }, 200);
  290. } else {
  291. callback();
  292. }
  293. };
  294. loop(function() {
  295. $(".ghic-participants").innerHTML = str;
  296. });
  297. }
  298.  
  299. function getSettings() {
  300. let name,
  301. keys = Object.keys(settings);
  302. for (name of keys) {
  303. settings[name].isHidden = GM_getValue(settings[name].name, false);
  304. }
  305. }
  306.  
  307. function saveSettings() {
  308. let name,
  309. keys = Object.keys(settings);
  310. for (name of keys) {
  311. GM_setValue(settings[name].name, settings[name].isHidden);
  312. }
  313. }
  314.  
  315. function getInputValues() {
  316. let name, item,
  317. keys = Object.keys(settings),
  318. menu = $(".ghic-menu");
  319. for (name of keys) {
  320. if (!(name === "pipeline" && !hasZenHub)) {
  321. item = closest($("." + settings[name].name, menu), ".dropdown-item");
  322. settings[name].isHidden = $("input", item).checked;
  323. toggleClass(item, "ghic-checked", settings[name].isHidden);
  324. }
  325. }
  326. }
  327.  
  328. function hideStuff(name, init) {
  329. let count, results,
  330. obj = settings[name],
  331. isHidden = obj.isHidden,
  332. item = closest($(".ghic-menu ." + obj.name), ".dropdown-item");
  333. if (obj.selector) {
  334. results = $$(obj.selector);
  335. toggleClass(item, "ghic-checked", isHidden);
  336. if (isHidden) {
  337. count = addClass(results, "ghic-hidden");
  338. $(".ghic-count", item).textContent = count ? "(" + count + ")" : " ";
  339. } else if (!init) {
  340. // no need to remove classes on initialization
  341. removeClass(results, "ghic-hidden");
  342. }
  343. toggleClass(item, "ghic-has-content", results.length);
  344. } else if (name === "plus1") {
  345. hidePlus1(init);
  346. } else if (name === "reactions") {
  347. toggleClass($("body"), "ghic-hideReactions", isHidden);
  348. toggleClass(item, "ghic-has-content", $$(".has-reactions").length - 1);
  349. // make first comment reactions visible
  350. item = $(".has-reactions", $(".timeline-comment-wrapper"));
  351. if (item) {
  352. item.style.display = "block";
  353. }
  354. }
  355. }
  356.  
  357. function hidePlus1(init) {
  358. if (init && !settings.plus1.isHidden) { return; }
  359. let max,
  360. indx = 0,
  361. count = 0,
  362. total = 0,
  363. // keep a list of post authors to prevent duplicate +1 counts
  364. authors = [],
  365. // used https://github.com/isaacs/github/issues/215 for matches here...
  366. // matches "+1!!!!", "++1", "+!", "+99!!!", "-1", "+ 100", "thumbs up"; ":+1:^21425235"
  367. // ignoring -1's... add unicode for thumbs up; it gets replaced with an image in Windows
  368. regexPlus = /([?!,.:^[\]()\'\"+-\d]|bump|thumbs|up|\ud83d\udc4d)/gi,
  369. // other comments to hide - they are still counted towards the +1 counter (for now?)
  370. // seen "^^^" to bump posts; "bump plleeaaassee"; "eta?"; "pretty please"
  371. // "need this"; "right now"; "still nothing?"; "super helpful"; "for gods sake"
  372. regexHide = new RegExp("(" + [
  373. "@\\w+",
  374. "pretty",
  375. "pl+e+a+s+e+",
  376. "y+e+s+",
  377. "eta",
  378. "much",
  379. "need(ed)?",
  380. "fix",
  381. "this",
  382. "right",
  383. "now",
  384. "still",
  385. "nothing",
  386. "super",
  387. "helpful",
  388. "for\\sgods\\ssake"
  389. ].join("|") + ")", "gi"),
  390. // image title ":{anything}:", etc.
  391. regexEmoji = /:(.*):/,
  392.  
  393. comments = $$(".js-discussion .timeline-comment-wrapper"),
  394. len = comments.length,
  395.  
  396. loop = function() {
  397. let wrapper, el, tmp, txt, img, hasLink, dupe;
  398. max = 0;
  399. while (max < 20 && indx < len) {
  400. if (indx >= len) {
  401. return;
  402. }
  403. wrapper = comments[indx];
  404. // save author list to prevent repeat +1s
  405. el = $(".timeline-comment-header .author", wrapper);
  406. txt = (el ? el.textContent || "" : "").toLowerCase();
  407. dupe = true;
  408. if (txt && authors.indexOf(txt) < 0) {
  409. authors[authors.length] = txt;
  410. dupe = false;
  411. }
  412. el = $(".comment-body", wrapper);
  413. // ignore quoted messages, but get all fragments
  414. tmp = $$(".email-fragment", el);
  415. // some posts only contain a link to related issues; these should not be counted as a +1
  416. // see https://github.com/isaacs/github/issues/618#issuecomment-200869630
  417. hasLink = $$(tmp.length ? ".email-fragment .issue-link" : ".issue-link", el).length;
  418. if (tmp.length) {
  419. // ignore quoted messages
  420. txt = getAllText(tmp);
  421. } else {
  422. txt = el.textContent.trim();
  423. }
  424. if (!txt) {
  425. img = $("img", el);
  426. if (img) {
  427. txt = img.getAttribute("title") || img.getAttribute("alt");
  428. }
  429. }
  430. // remove fluff
  431. txt = txt.replace(regexEmoji, "").replace(regexPlus, "").replace(regexHide, "").trim();
  432. if (txt === "" || (txt.length < 4 && !hasLink)) {
  433. if (settings.plus1.isHidden) {
  434. wrapper.classList.add("ghic-hidden");
  435. total++;
  436. // one +1 per author
  437. if (!dupe) {
  438. count++;
  439. }
  440. } else if (!init) {
  441. wrapper.classList.remove("ghic-hidden");
  442. }
  443. max++;
  444. }
  445. indx++;
  446. }
  447. if (indx < len) {
  448. setTimeout(function() {
  449. loop();
  450. }, 200);
  451. } else {
  452. $(".ghic-menu .ghic-plus1 .ghic-count").textContent = total ? "(" + total + ")" : " ";
  453. toggleClass($(".ghic-menu ." + settings.plus1.name), "ghic-has-content", total);
  454. addCountToReaction(count);
  455. }
  456. };
  457. loop();
  458. }
  459.  
  460. function getAllText(el) {
  461. let txt = "",
  462. indx = el.length;
  463. // text order doesn't matter
  464. while (indx--) {
  465. txt += el[indx].textContent.trim();
  466. }
  467. return txt;
  468. }
  469.  
  470. function addCountToReaction(count) {
  471. if (!count) {
  472. count = ($(".ghic-menu .ghic-plus1 .ghic-count").textContent || "")
  473. .replace(/[()]/g, "")
  474. .trim();
  475. }
  476. let comment = $(".timeline-comment"),
  477. tmp = $(".has-reactions button[value='+1 react'], .has-reactions button[value='+1 unreact']", comment),
  478. el = $(".ghic-count", comment);
  479. if (el) {
  480. // the count may have been appended to the comment & now
  481. // there is a reaction, so remove any "ghic-count" elements
  482. el.parentNode.removeChild(el);
  483. }
  484. if (count) {
  485. if (tmp) {
  486. el = document.createElement("span");
  487. el.className = "ghic-count";
  488. el.textContent = count ? " + " + count + " (from hidden comments)" : "";
  489. tmp.appendChild(el);
  490. } else {
  491. el = document.createElement("p");
  492. el.className = "ghic-count";
  493. el.innerHTML = "<hr>" + plus1Icon + " " + count + " (from hidden comments)";
  494. $(".comment-body", comment).appendChild(el);
  495. }
  496. }
  497. }
  498.  
  499. function hideParticipant(el) {
  500. let els, indx, len, hide, name,
  501. results = [];
  502. if (el) {
  503. el.classList.toggle("comments-hidden");
  504. hide = el.classList.contains("comments-hidden");
  505. name = el.getAttribute("aria-label");
  506. els = $$(".js-discussion .author");
  507. len = els.length;
  508. for (indx = 0; indx < len; indx++) {
  509. if (els[indx].textContent.trim() === name) {
  510. results[results.length] = closest(els[indx], ".timeline-comment-wrapper, .commit-comment, .discussion-item");
  511. }
  512. }
  513. // use a different participant class name to hide timeline events
  514. // or unselecting all users will show everything
  515. if (el.classList.contains("comments-hidden")) {
  516. addClass(results, "ghic-hidden-participant");
  517. } else {
  518. removeClass(results, "ghic-hidden-participant");
  519. }
  520. results = [];
  521. }
  522. }
  523.  
  524. function update() {
  525. busy = true;
  526. if ($("#discussion_bucket") && $(".ghic-button")) {
  527. let keys = Object.keys(settings),
  528. indx = keys.length;
  529. while (indx--) {
  530. // true flag for init - no need to remove classes
  531. hideStuff(keys[indx], true);
  532. }
  533. }
  534. busy = false;
  535. }
  536.  
  537. function checkItem(event) {
  538. busy = true;
  539. if (document.getElementById("discussion_bucket")) {
  540. let name,
  541. target = event.target,
  542. wrap = target && target.parentNode;
  543. if (target && wrap) {
  544. if (target.nodeName === "INPUT" && wrap.classList.contains("ghic-right")) {
  545. getInputValues();
  546. saveSettings();
  547. // extract ghic-{name}, because it matches the name in settings
  548. name = wrap.className.replace("ghic-right", "").replace("ghic-has-content", "").trim();
  549. if (wrap.classList.contains(name)) {
  550. hideStuff(name.replace("ghic-", ""));
  551. }
  552. } else if (target.classList.contains("ghic-avatar")) {
  553. // make sure we're targeting the span wrapping the image
  554. hideParticipant(target.nodeName === "IMG" ? target.parentNode : target);
  555. } else if (regex.test(target.nodeName)) {
  556. // clicking on the SVG may target the svg or path inside
  557. hideParticipant(closest(target, ".ghic-avatar"));
  558. }
  559. }
  560. }
  561. busy = false;
  562. }
  563.  
  564. function init() {
  565. busy = true;
  566. getSettings();
  567. addMenu();
  568. $("body").addEventListener("input", checkItem);
  569. $("body").addEventListener("click", checkItem);
  570. update();
  571. busy = false;
  572. }
  573.  
  574. // DOM targets - to detect GitHub dynamic ajax page loading
  575. targets = $$("#js-repo-pjax-container, #js-pjax-container, .js-discussion");
  576.  
  577. // update TOC when content changes
  578. Array.prototype.forEach.call(targets, function(target) {
  579. new MutationObserver(function(mutations) {
  580. mutations.forEach(function(mutation) {
  581. // preform checks before adding code wrap to minimize function calls
  582. if (!busy && mutation.target === target) {
  583. addMenu();
  584. }
  585. });
  586. }).observe(target, {
  587. childList: true,
  588. subtree: true
  589. });
  590. });
  591.  
  592. init();
  593.  
  594. })();