GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

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

  1. // ==UserScript==
  2. // @name GitHub Toggle Issue Comments
  3. // @version 1.0.5
  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.  
  49. settings = {
  50. // https://github.com/Mottie/Keyboard/issues/448
  51. title: {
  52. isHidden: false,
  53. name: "ghic-title",
  54. selector: ".discussion-item-renamed",
  55. label: "Title Changes"
  56. },
  57. labels: {
  58. isHidden: false,
  59. name: "ghic-labels",
  60. selector: ".discussion-item-labeled, .discussion-item-unlabeled",
  61. label: "Label Changes"
  62. },
  63. state: {
  64. isHidden: false,
  65. name: "ghic-state",
  66. selector: ".discussion-item-reopened, .discussion-item-closed",
  67. label: "State Changes (close/reopen)"
  68. },
  69.  
  70. // https://github.com/jquery/jquery/issues/2986
  71. milestone: {
  72. isHidden: false,
  73. name: "ghic-milestone",
  74. selector: ".discussion-item-milestoned",
  75. label: "Milestone Changes"
  76. },
  77. refs: {
  78. isHidden: false,
  79. name: "ghic-refs",
  80. selector: ".discussion-item-ref, .discussion-item-head_ref_deleted",
  81. label: "References"
  82. },
  83. assigned: {
  84. isHidden: false,
  85. name: "ghic-assigned",
  86. selector: ".discussion-item-assigned",
  87. label: "Assignment Changes"
  88. },
  89.  
  90. // Pull Requests
  91. commits: {
  92. isHidden: false,
  93. name: "ghic-commits",
  94. selector: ".discussion-commits",
  95. label: "Commits"
  96. },
  97. // https://github.com/jquery/jquery/pull/3014
  98. diffOld: {
  99. isHidden: false,
  100. name: "ghic-diffOld",
  101. selector: ".outdated-diff-comment-container",
  102. label: "Diff (outdated) Comments"
  103. },
  104. diffNew: {
  105. isHidden: false,
  106. name: "ghic-diffNew",
  107. selector: "[id^=diff-for-comment-]:not(.outdated-diff-comment-container)",
  108. label: "Diff (current) Comments"
  109. },
  110. // https://github.com/jquery/jquery/pull/2949
  111. merged: {
  112. isHidden: false,
  113. name: "ghic-merged",
  114. selector: ".discussion-item-merged",
  115. label: "Merged"
  116. },
  117. integrate: {
  118. isHidden: false,
  119. name: "ghic-integrate",
  120. selector: ".discussion-item-integrations-callout",
  121. label: "Integrations"
  122. },
  123.  
  124. // extras (special treatment - no selector)
  125. plus1: {
  126. isHidden: false,
  127. name: "ghic-plus1",
  128. label: "Hide +1s"
  129. },
  130. reactions: {
  131. isHidden: false,
  132. name: "ghic-reactions",
  133. label: "Reactions"
  134. },
  135. // page with lots of users to hide:
  136. // https://github.com/isaacs/github/issues/215
  137.  
  138. // ZenHub pipeline change
  139. pipeline: {
  140. isHidden: false,
  141. name: "ghic-pipeline",
  142. selector: ".discussion-item.zh-discussion-item",
  143. label: "ZenHub Pipeline Changes"
  144. }
  145. },
  146.  
  147. matches = function(el, selector) {
  148. // https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
  149. var matches = document.querySelectorAll(selector),
  150. i = matches.length;
  151. while (--i >= 0 && matches.item(i) !== el) {}
  152. return i > -1;
  153. },
  154.  
  155. closest = function(el, selector) {
  156. while (el && !matches(el, selector)) {
  157. el = el.parentNode;
  158. }
  159. return matches(el, selector) ? el : null;
  160. },
  161.  
  162. addClass = function(els, name) {
  163. var indx,
  164. len = els.length;
  165. for (indx = 0; indx < len; indx++) {
  166. els[indx].classList.add(name);
  167. }
  168. return len;
  169. },
  170.  
  171. removeClass = function(els, name) {
  172. var indx,
  173. len = els.length;
  174. for (indx = 0; indx < len; indx++) {
  175. els[indx].classList.remove(name);
  176. }
  177. },
  178.  
  179. addMenu = function() {
  180. busy = true;
  181. if (document.getElementById("discussion_bucket") && !document.querySelector(".ghic-button")) {
  182. // update "isHidden" values
  183. getSettings();
  184. var name,
  185. list = "",
  186. header = document.querySelector(".discussion-sidebar-item:last-child"),
  187. menu = document.createElement("div");
  188.  
  189. for (name in settings) {
  190. if (settings.hasOwnProperty(name) && !(name === "pipeline" && !hasZenHub)) {
  191. list += "<label class='dropdown-item'>" + settings[name].label +
  192. "<span class='ghic-right " + settings[name].name + "'>" +
  193. "<input type='checkbox'" + (settings[name].isHidden ? " checked" : "") + ">" +
  194. iconCheck + "<span class='ghic-count'> </span></span></label>";
  195. }
  196. }
  197.  
  198. menu.className = "ghic-button";
  199. menu.innerHTML = [
  200. "<span class='btn btn-sm' role='button' tabindex='0' aria-haspopup='true'>",
  201. "<span class='tooltipped tooltipped-w' aria-label='Toggle issue comments'>",
  202. "<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>",
  203. "</span>",
  204. "<div class='select-menu-modal-holder'>",
  205. "<div class='select-menu-modal' aria-hidden='true'>",
  206. "<div class='select-menu-header' tabindex='-1'>",
  207. "<span class='select-menu-title'>Toggle items</span>",
  208. "</div>",
  209. "<div class='select-menu-list ghic-menu' role='menu'>",
  210. list,
  211. "<div class='ghic-participants'></div>",
  212. "</div>",
  213. "</div>",
  214. "</div>",
  215. "</span>"
  216. ].join("");
  217. if (hasZenHub) {
  218. header.insertBefore(menu, header.childNodes[0]);
  219. } else {
  220. header.appendChild(menu);
  221. }
  222. addAvatars();
  223. update();
  224. }
  225. busy = false;
  226. },
  227.  
  228. addAvatars = function() {
  229. var indx = 0,
  230.  
  231. str = "<h3>Hide Comments from</h3>",
  232. unique = [],
  233. // get all avatars
  234. avatars = document.querySelectorAll(".timeline-comment-avatar"),
  235. len = avatars.length - 1, // last avatar is the new comment with the current user
  236.  
  237. loop = function(callback) {
  238. var el, name,
  239. max = 0;
  240. while (max < 50 && indx < len) {
  241. if (indx >= len) {
  242. return callback();
  243. }
  244. el = avatars[indx];
  245. name = (el.getAttribute("alt") || "").replace("@", "");
  246. if (unique.indexOf(name) < 0) {
  247. str += "<span class='ghic-avatar tooltipped tooltipped-n' aria-label='" + name + "'>" +
  248. iconHidden +
  249. "<img class='ghic-avatar avatar' width='24' height='24' src='" + el.src + "'/>" +
  250. "</span>";
  251. unique[unique.length] = name;
  252. max++;
  253. }
  254. indx++;
  255. }
  256. if (indx < len) {
  257. setTimeout(function() {
  258. loop(callback);
  259. }, 200);
  260. } else {
  261. callback();
  262. }
  263. };
  264. loop(function() {
  265. document.querySelector(".ghic-participants").innerHTML = str;
  266. });
  267. },
  268.  
  269. getSettings = function() {
  270. var name;
  271. for (name in settings) {
  272. if (settings.hasOwnProperty(name)) {
  273. settings[name].isHidden = GM_getValue(settings[name].name, false);
  274. }
  275. }
  276. },
  277.  
  278. saveSettings = function() {
  279. var name;
  280. for (name in settings) {
  281. if (settings.hasOwnProperty(name)) {
  282. GM_setValue(settings[name].name, settings[name].isHidden);
  283. }
  284. }
  285. },
  286.  
  287. getInputValues = function() {
  288. var name,
  289. menu = document.querySelector(".ghic-menu");
  290. for (name in settings) {
  291. if (settings.hasOwnProperty(name) && !(name === "pipeline" && !hasZenHub)) {
  292. settings[name].isHidden = menu.querySelector("." + settings[name].name + " input").checked;
  293. }
  294. }
  295. },
  296.  
  297. hideStuff = function(name, init) {
  298. if (settings[name].selector) {
  299. var count,
  300. results = document.querySelectorAll(settings[name].selector);
  301. if (settings[name].isHidden) {
  302. count = addClass(results, "ghic-hidden");
  303. document.querySelector(".ghic-menu ." + settings[name].name + " .ghic-count")
  304. .textContent = count ? "(" + count + ")" : " ";
  305. } else if (!init) {
  306. // no need to remove classes on initialization
  307. removeClass(results, "ghic-hidden");
  308. }
  309. } else if (name === "plus1") {
  310. hidePlus1(init);
  311. } else if (name === "reactions") {
  312. document.querySelector("body").classList[settings[name].isHidden ? "add" : "remove"]("ghic-hideReactions");
  313. }
  314. },
  315.  
  316. hidePlus1 = function(init) {
  317. if (init && !settings.plus1.isHidden) { return; }
  318. var max,
  319. indx = 0,
  320. count = 0,
  321. // used https://github.com/isaacs/github/issues/215 for matches here...
  322. // matches "+1!!!!", "++1", "+!", "+99!!!", "-1", "+ 100", etc
  323. // seen "^^^" to bump posts; "bump plleeaaassee"; "eta?"
  324. regexStr = /([?!,.:^[\]+-019]|bump|pl+e+a+s+e+|eta)/gi,
  325. // image title ":{anything}:", etc.
  326. regexEmoji = /:(.*):/,
  327. comments = document.querySelectorAll(".timeline-comment-wrapper .comment-body"),
  328. len = comments.length,
  329.  
  330. loop = function() {
  331. var el, txt, img;
  332. max = 0;
  333. while (max < 20 && indx < len) {
  334. if (indx >= len) {
  335. return;
  336. }
  337. el = comments[indx];
  338. if (el.querySelector(".email-quoted-reply")) {
  339. // ignore quoted messages
  340. txt = el.querySelector(".email-fragment").textContent.trim();
  341. } else {
  342. txt = el.textContent.trim();
  343. }
  344. if (!txt) {
  345. img = el.querySelector("img");
  346. if (img) {
  347. txt = img.getAttribute("title") || img.getAttribute("alt");
  348. }
  349. }
  350. // remove fluff
  351. txt = txt.replace(regexEmoji, "").replace(regexStr, "").trim();
  352. if (txt === "" || txt.length < 5) {
  353. if (settings.plus1.isHidden) {
  354. closest(el, ".timeline-comment-wrapper").classList.add("ghic-hidden");
  355. count++;
  356. } else if (!init) {
  357. closest(el, ".timeline-comment-wrapper").classList.remove("ghic-hidden");
  358. }
  359. max++;
  360. }
  361. indx++;
  362. }
  363. if (indx < len) {
  364. setTimeout(function() {
  365. loop();
  366. }, 200);
  367. } else {
  368. document.querySelector(".ghic-menu .ghic-plus1 .ghic-count")
  369. .textContent = count ? "(" + count + ")" : " ";
  370. }
  371. };
  372. loop();
  373. },
  374.  
  375. hideParticipant = function(el) {
  376. var els, indx, len, hide, name,
  377. results = [];
  378. if (el) {
  379. el.classList.toggle("comments-hidden");
  380. hide = el.classList.contains("comments-hidden");
  381. name = el.getAttribute("aria-label");
  382. els = document.querySelectorAll(".js-discussion .author");
  383. len = els.length;
  384. for (indx = 0; indx < len; indx++) {
  385. if (els[indx].textContent.trim() === name) {
  386. results[results.length] = closest(els[indx], ".timeline-comment-wrapper, .commit-comment, .discussion-item");
  387. }
  388. }
  389. // use a different participant class name to hide timeline events
  390. // or unselecting all users will show everything
  391. if (el.classList.contains("comments-hidden")) {
  392. addClass(results, "ghic-hidden-participant");
  393. } else {
  394. removeClass(results, "ghic-hidden-participant");
  395. }
  396. results = [];
  397. }
  398. },
  399.  
  400. regex = /(svg|path)/i,
  401.  
  402. update = function() {
  403. var keys = Object.keys(settings),
  404. indx = keys.length;
  405. while (indx--) {
  406. // true flag for init - no need to remove classes
  407. hideStuff(keys[indx], true);
  408. }
  409. },
  410.  
  411. checkItem = function(event) {
  412. busy = true;
  413. if (document.getElementById("discussion_bucket")) {
  414. var name,
  415. target = event.target,
  416. wrap = target && target.parentNode;
  417. if (target && wrap) {
  418. if (target.nodeName === "INPUT" && wrap.classList.contains("ghic-right")) {
  419. getInputValues();
  420. saveSettings();
  421. // extract ghic-{name}, because it matches the name in settings
  422. name = wrap.className.replace("ghic-right", "").trim();
  423. if (wrap.classList.contains(name)) {
  424. hideStuff(name.replace("ghic-", ""));
  425. }
  426. } else if (target.classList.contains("ghic-avatar")) {
  427. // make sure we're targeting the span wrapping the image
  428. hideParticipant(target.nodeName === "IMG" ? target.parentNode : target);
  429. } else if (regex.test(target.nodeName)) {
  430. // clicking on the SVG may target the svg or path inside
  431. hideParticipant(closest(target, ".ghic-avatar"));
  432. }
  433. }
  434. }
  435. busy = false;
  436. },
  437.  
  438. init = function() {
  439. busy = true;
  440. getSettings();
  441. addMenu();
  442. document.querySelector("body").addEventListener("input", checkItem);
  443. document.querySelector("body").addEventListener("click", checkItem);
  444. update();
  445. busy = false;
  446. },
  447.  
  448. // DOM targets - to detect GitHub dynamic ajax page loading
  449. targets = document.querySelectorAll([
  450. "#js-repo-pjax-container",
  451. "#js-pjax-container"
  452. ].join(","));
  453.  
  454. // update TOC when content changes
  455. Array.prototype.forEach.call(targets, function(target) {
  456. new MutationObserver(function(mutations) {
  457. mutations.forEach(function(mutation) {
  458. // preform checks before adding code wrap to minimize function calls
  459. if (!busy && mutation.target === target) {
  460. addMenu();
  461. }
  462. });
  463. }).observe(target, {
  464. childList: true,
  465. subtree: true
  466. });
  467. });
  468.  
  469. init();
  470.  
  471. })();