GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

Version au 23/06/2016. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

  1. // ==UserScript==
  2. // @name GitHub Toggle Issue Comments
  3. // @version 1.0.14
  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://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 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);
  349. }
  350. }
  351.  
  352. function hidePlus1(init) {
  353. if (init && !settings.plus1.isHidden) { return; }
  354. let max,
  355. indx = 0,
  356. count = 0,
  357. total = 0,
  358. // keep a list of post authors to prevent duplicate +1 counts
  359. authors = [],
  360. // used https://github.com/isaacs/github/issues/215 for matches here...
  361. // matches "+1!!!!", "++1", "+!", "+99!!!", "-1", "+ 100", "thumbs up"; ":+1:^21425235"
  362. // ignoring -1's...
  363. regexPlus = /([?!,.:^[\]()\'\"+-\d]|bump|thumbs|up)/gi,
  364. // other comments to hide - they are still counted towards the +1 counter (for now?)
  365. // seen "^^^" to bump posts; "bump plleeaaassee"; "eta?"; "pretty please"
  366. // "need this"; "right now"; "still nothing?"; "super helpful"; "for gods sake"
  367. regexHide = new RegExp("(" + [
  368. "@\\w+",
  369. "pretty",
  370. "pl+e+a+s+e+",
  371. "y+e+s+",
  372. "eta",
  373. "much",
  374. "need(ed)?",
  375. "fix",
  376. "this",
  377. "right",
  378. "now",
  379. "still",
  380. "nothing",
  381. "super",
  382. "helpful",
  383. "for\\sgods\\ssake"
  384. ].join("|") + ")", "gi"),
  385. // image title ":{anything}:", etc.
  386. regexEmoji = /:(.*):/,
  387.  
  388. comments = $$(".js-discussion .timeline-comment-wrapper"),
  389. len = comments.length,
  390.  
  391. loop = function() {
  392. let wrapper, el, tmp, txt, img, hasLink, dupe;
  393. max = 0;
  394. while (max < 20 && indx < len) {
  395. if (indx >= len) {
  396. return;
  397. }
  398. wrapper = comments[indx];
  399. // save author list to prevent repeat +1s
  400. el = $(".timeline-comment-header .author", wrapper);
  401. txt = (el ? el.textContent || "" : "").toLowerCase();
  402. dupe = true;
  403. if (txt && authors.indexOf(txt) < 0) {
  404. authors[authors.length] = txt;
  405. dupe = false;
  406. }
  407. el = $(".comment-body", wrapper);
  408. // ignore quoted messages, but get all fragments
  409. tmp = $$(".email-fragment", el);
  410. // some posts only contain a link to related issues; these should not be counted as a +1
  411. // see https://github.com/isaacs/github/issues/618#issuecomment-200869630
  412. hasLink = $$(tmp.length ? ".email-fragment .issue-link" : ".issue-link", el).length;
  413. if (tmp.length) {
  414. // ignore quoted messages
  415. txt = getAllText(tmp);
  416. } else {
  417. txt = el.textContent.trim();
  418. }
  419. if (!txt) {
  420. img = $("img", el);
  421. if (img) {
  422. txt = img.getAttribute("title") || img.getAttribute("alt");
  423. }
  424. }
  425. // remove fluff
  426. txt = txt.replace(regexEmoji, "").replace(regexPlus, "").replace(regexHide, "").trim();
  427. if (txt === "" || (txt.length < 4 && !hasLink)) {
  428. if (settings.plus1.isHidden) {
  429. wrapper.classList.add("ghic-hidden");
  430. total++;
  431. // one +1 per author
  432. if (!dupe) {
  433. count++;
  434. }
  435. } else if (!init) {
  436. wrapper.classList.remove("ghic-hidden");
  437. }
  438. max++;
  439. }
  440. indx++;
  441. }
  442. if (indx < len) {
  443. setTimeout(function() {
  444. loop();
  445. }, 200);
  446. } else {
  447. $(".ghic-menu .ghic-plus1 .ghic-count").textContent = total ? "(" + total + ")" : " ";
  448. toggleClass($(".ghic-menu ." + settings.plus1.name), "ghic-has-content", total);
  449. addCountToReaction(count);
  450. }
  451. };
  452. loop();
  453. }
  454.  
  455. function getAllText(el) {
  456. let txt = "",
  457. indx = el.length;
  458. // text order doesn't matter
  459. while (indx--) {
  460. txt += el[indx].textContent.trim();
  461. }
  462. return txt;
  463. }
  464.  
  465. function addCountToReaction(count) {
  466. if (!count) {
  467. count = ($(".ghic-menu .ghic-plus1 .ghic-count").textContent || "")
  468. .replace(/[()]/g, "")
  469. .trim();
  470. }
  471. let comment = $(".timeline-comment"),
  472. tmp = $(".has-reactions button[value='+1 react'], .has-reactions button[value='+1 unreact']", comment),
  473. el = $(".ghic-count", comment);
  474. if (el) {
  475. // the count may have been appended to the comment & now
  476. // there is a reaction, so remove any "ghic-count" elements
  477. el.parentNode.removeChild(el);
  478. }
  479. if (count) {
  480. if (tmp) {
  481. el = document.createElement("span");
  482. el.className = "ghic-count";
  483. el.textContent = count ? " + " + count + " (from hidden comments)" : "";
  484. tmp.appendChild(el);
  485. } else {
  486. el = document.createElement("p");
  487. el.className = "ghic-count";
  488. el.innerHTML = "<hr>" + plus1Icon + " " + count + " (from hidden comments)";
  489. $(".comment-body", comment).appendChild(el);
  490. }
  491. }
  492. }
  493.  
  494. function hideParticipant(el) {
  495. let els, indx, len, hide, name,
  496. results = [];
  497. if (el) {
  498. el.classList.toggle("comments-hidden");
  499. hide = el.classList.contains("comments-hidden");
  500. name = el.getAttribute("aria-label");
  501. els = $$(".js-discussion .author");
  502. len = els.length;
  503. for (indx = 0; indx < len; indx++) {
  504. if (els[indx].textContent.trim() === name) {
  505. results[results.length] = closest(els[indx], ".timeline-comment-wrapper, .commit-comment, .discussion-item");
  506. }
  507. }
  508. // use a different participant class name to hide timeline events
  509. // or unselecting all users will show everything
  510. if (el.classList.contains("comments-hidden")) {
  511. addClass(results, "ghic-hidden-participant");
  512. } else {
  513. removeClass(results, "ghic-hidden-participant");
  514. }
  515. results = [];
  516. }
  517. }
  518.  
  519. function update() {
  520. busy = true;
  521. if ($("#discussion_bucket") && $(".ghic-button")) {
  522. let keys = Object.keys(settings),
  523. indx = keys.length;
  524. while (indx--) {
  525. // true flag for init - no need to remove classes
  526. hideStuff(keys[indx], true);
  527. }
  528. }
  529. busy = false;
  530. }
  531.  
  532. function checkItem(event) {
  533. busy = true;
  534. if (document.getElementById("discussion_bucket")) {
  535. let name,
  536. target = event.target,
  537. wrap = target && target.parentNode;
  538. if (target && wrap) {
  539. if (target.nodeName === "INPUT" && wrap.classList.contains("ghic-right")) {
  540. getInputValues();
  541. saveSettings();
  542. // extract ghic-{name}, because it matches the name in settings
  543. name = wrap.className.replace("ghic-right", "").trim();
  544. if (wrap.classList.contains(name)) {
  545. hideStuff(name.replace("ghic-", ""));
  546. }
  547. } else if (target.classList.contains("ghic-avatar")) {
  548. // make sure we're targeting the span wrapping the image
  549. hideParticipant(target.nodeName === "IMG" ? target.parentNode : target);
  550. } else if (regex.test(target.nodeName)) {
  551. // clicking on the SVG may target the svg or path inside
  552. hideParticipant(closest(target, ".ghic-avatar"));
  553. }
  554. }
  555. }
  556. busy = false;
  557. }
  558.  
  559. function init() {
  560. busy = true;
  561. getSettings();
  562. addMenu();
  563. $("body").addEventListener("input", checkItem);
  564. $("body").addEventListener("click", checkItem);
  565. update();
  566. busy = false;
  567. }
  568.  
  569. // DOM targets - to detect GitHub dynamic ajax page loading
  570. targets = $$("#js-repo-pjax-container, #js-pjax-container, .js-discussion");
  571.  
  572. // update TOC when content changes
  573. Array.prototype.forEach.call(targets, function(target) {
  574. new MutationObserver(function(mutations) {
  575. mutations.forEach(function(mutation) {
  576. // preform checks before adding code wrap to minimize function calls
  577. if (!busy && mutation.target === target) {
  578. addMenu();
  579. }
  580. });
  581. }).observe(target, {
  582. childList: true,
  583. subtree: true
  584. });
  585. });
  586.  
  587. init();
  588.  
  589. })();