Greasy Fork 还支持 简体中文。

GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

目前為 2017-03-25 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Toggle Issue Comments
  3. // @version 1.0.21
  4. // @description A userscript that toggles issues/pull request comments & messages
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace https://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. (() => {
  15. "use strict";
  16.  
  17. GM_addStyle(`
  18. .ghic-button { float:right; }
  19. .ghic-button .btn:hover div.select-menu-modal-holder { display:block; top:auto; bottom:25px; right:0; }
  20. .ghic-right { float:right; }
  21. /* pre-wrap set for Firefox; see https://greasyfork.org/en/forum/discussion/9166/x */
  22. .ghic-menu label { display:block; padding:5px 15px; white-space:pre-wrap; }
  23. .ghic-button .select-menu-header, .ghic-participants { cursor:default; }
  24. .ghic-participants { border-top:1px solid #484848; padding:15px; }
  25. .ghic-avatar { display:inline-block; float:left; margin: 0 2px 2px 0; cursor:pointer; position:relative; }
  26. .ghic-avatar:last-child { margin-bottom:5px; }
  27. .ghic-avatar.comments-hidden svg { display:block; position:absolute; top:-2px; left:-2px; z-index:1; }
  28. .ghic-avatar.comments-hidden img { opacity:0.5; }
  29. .ghic-button .dropdown-item span { font-weight:normal; opacity:.5; }
  30. .ghic-button .dropdown-item.ghic-has-content span { opacity:1; }
  31. .ghic-button .dropdown-item.ghic-checked span { font-weight:bold; }
  32. .ghic-button .dropdown-item.ghic-checked svg,
  33. .ghic-button .dropdown-item.ghic-checked .ghic-count { display:inline-block; }
  34. .ghic-button .ghic-count { float:left; margin-right:5px; }
  35. .ghic-button .select-menu-modal { margin:0; }
  36. .ghic-button .ghic-participants { margin-bottom:20px; }
  37. /* for testing: ".ghic-hidden { opacity: 0.3; } */
  38. .ghic-hidden, .ghic-hidden-participant, .ghic-avatar svg, .ghic-button .ghic-right > *,
  39. .ghic-hideReactions .comment-reactions { display:none; }
  40. `);
  41.  
  42. let debounce,
  43. busy = false,
  44. observers = [];
  45.  
  46. const regex = /(svg|path)/i,
  47. // ZenHub addon active (include ZenHub Enterprise)
  48. hasZenHub = $(".zhio, .zhe") ? true : false,
  49.  
  50. settings = {
  51. // example: 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. // example: 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. // example: 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. // example: 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. 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>`,
  149. 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>`,
  150. 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">`;
  151.  
  152. function addMenu() {
  153. busy = true;
  154. if ($("#discussion_bucket") && !$(".ghic-button")) {
  155. addObservers();
  156. // update "isHidden" values
  157. getSettings();
  158. let name, bright, isHidden, isChecked,
  159. list = "",
  160. keys = Object.keys(settings),
  161. header = $(".discussion-sidebar-item:last-child"),
  162. menu = document.createElement("div");
  163.  
  164. for (name of keys) {
  165. if (!(name === "pipeline" && !hasZenHub)) {
  166. // make plus1 and reactions list items always bright
  167. bright = name === "plus1" ? " ghic-has-content" : "";
  168. isHidden = settings[name].isHidden;
  169. isChecked = isHidden ? " ghic-checked": "";
  170. // not using multi-line backticks because it adds lots of white-space to the label
  171. list += `<label class="dropdown-item${bright}${isChecked}">` +
  172. `<span>${settings[name].label}</span>` +
  173. `<span class="ghic-right ${settings[name].name}">` +
  174. `<input type="checkbox"${isHidden ? " checked" : ""}>` +
  175. `${iconCheck}<span class="ghic-count"> </span>` +
  176. `</span></label>`;
  177. }
  178. }
  179.  
  180. menu.className = "ghic-button";
  181. menu.innerHTML = `
  182. <span class="btn btn-sm" role="button" tabindex="0" aria-haspopup="true">
  183. <span class="tooltipped tooltipped-w" aria-label="Toggle issue comments">
  184. <svg class="octicon octicon-comment-discussion" height="16" width="16" role="img" viewBox="0 0 16 16">
  185. <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>
  186. </svg>
  187. </span>
  188. <div class="select-menu-modal-holder">
  189. <div class="select-menu-modal" aria-hidden="true">
  190. <div class="select-menu-header" tabindex="-1">
  191. <span class="select-menu-title">Toggle items</span>
  192. </div>
  193. <div class="select-menu-list ghic-menu" role="menu">
  194. ${list}
  195. <div class="ghic-participants"></div>
  196. </div>
  197. </div>
  198. </div>
  199. </span>
  200. `;
  201. if (hasZenHub) {
  202. header.insertBefore(menu, header.childNodes[0]);
  203. } else {
  204. header.appendChild(menu);
  205. }
  206. addAvatars();
  207. }
  208. update();
  209. busy = false;
  210. }
  211.  
  212. function addAvatars() {
  213. let indx = 0,
  214.  
  215. str = "<h3>Hide Comments from</h3>",
  216. unique = [],
  217. // get all avatars
  218. avatars = $$(".timeline-comment-avatar"),
  219. len = avatars.length - 1, // last avatar is the new comment with the current user
  220.  
  221. loop = (callback) => {
  222. let el, name,
  223. max = 0;
  224. while (max < 50 && indx < len) {
  225. if (indx >= len) {
  226. return callback();
  227. }
  228. el = avatars[indx];
  229. name = (el.getAttribute("alt") || "").replace("@", "");
  230. if (unique.indexOf(name) < 0) {
  231. str += `<span class="ghic-avatar tooltipped tooltipped-n" aria-label="${name}">
  232. ${iconHidden}
  233. <img class="ghic-avatar avatar" width="24" height="24" src="${el.src}"/>
  234. </span>`;
  235. unique[unique.length] = name;
  236. max++;
  237. }
  238. indx++;
  239. }
  240. if (indx < len) {
  241. setTimeout(() => {
  242. loop(callback);
  243. }, 200);
  244. } else {
  245. callback();
  246. }
  247. };
  248. loop(() => {
  249. $(".ghic-participants").innerHTML = str;
  250. });
  251. }
  252.  
  253. function getSettings() {
  254. let name,
  255. keys = Object.keys(settings);
  256. for (name of keys) {
  257. settings[name].isHidden = GM_getValue(settings[name].name, false);
  258. }
  259. }
  260.  
  261. function saveSettings() {
  262. let name,
  263. keys = Object.keys(settings);
  264. for (name of keys) {
  265. GM_setValue(settings[name].name, settings[name].isHidden);
  266. }
  267. }
  268.  
  269. function getInputValues() {
  270. let name, item,
  271. keys = Object.keys(settings),
  272. menu = $(".ghic-menu");
  273. for (name of keys) {
  274. if (!(name === "pipeline" && !hasZenHub)) {
  275. item = closest(".dropdown-item", $("." + settings[name].name, menu));
  276. settings[name].isHidden = $("input", item).checked;
  277. toggleClass(item, "ghic-checked", settings[name].isHidden);
  278. }
  279. }
  280. }
  281.  
  282. function hideStuff(name, init) {
  283. let count, results,
  284. obj = settings[name],
  285. isHidden = obj.isHidden,
  286. item = closest(".dropdown-item", $(".ghic-menu ." + obj.name));
  287. if (obj.selector) {
  288. results = $$(obj.selector);
  289. toggleClass(item, "ghic-checked", isHidden);
  290. if (isHidden) {
  291. count = addClass(results, "ghic-hidden");
  292. $(".ghic-count", item).textContent = count ? "(" + count + ")" : " ";
  293. } else if (!init) {
  294. // no need to remove classes on initialization
  295. removeClass(results, "ghic-hidden");
  296. }
  297. toggleClass(item, "ghic-has-content", results.length);
  298. } else if (name === "plus1") {
  299. hidePlus1(init);
  300. } else if (name === "reactions") {
  301. toggleClass($("body"), "ghic-hideReactions", isHidden);
  302. toggleClass(item, "ghic-has-content", $$(".has-reactions").length - 1);
  303. // make first comment reactions visible
  304. item = $(".has-reactions", $(".timeline-comment-wrapper"));
  305. if (item) {
  306. item.style.display = "block";
  307. }
  308. }
  309. }
  310.  
  311. function hidePlus1(init) {
  312. if (init && !settings.plus1.isHidden) {
  313. return;
  314. }
  315. let max,
  316. indx = 0,
  317. count = 0,
  318. total = 0,
  319. // keep a list of post authors to prevent duplicate +1 counts
  320. authors = [],
  321. // used https://github.com/isaacs/github/issues/215 for matches here...
  322. // matches "+1!!!!", "++1", "+!", "+99!!!", "-1", "+ 100", "thumbs up"; ":+1:^21425235"
  323. // ignoring -1's... add unicode for thumbs up; it gets replaced with an image in Windows
  324. regexPlus = /([?!,.:^[\]()\'\"+-\d]|bump|thumbs|up|\ud83d\udc4d)/gi,
  325. // other comments to hide - they are still counted towards the +1 counter (for now?)
  326. // seen "^^^" to bump posts; "bump plleeaaassee"; "eta?"; "pretty please"
  327. // "need this"; "right now"; "still nothing?"; "super helpful"; "for gods sake"
  328. regexHide = new RegExp("(" + [
  329. "@\\w+",
  330. "pretty",
  331. "pl+e+a+s+e+",
  332. "y+e+s+",
  333. "eta",
  334. "much",
  335. "need(ed)?",
  336. "fix",
  337. "this",
  338. "right",
  339. "now",
  340. "still",
  341. "nothing",
  342. "super",
  343. "helpful",
  344. "for\\sgods\\ssake",
  345. "c'?mon",
  346. "come\\son"
  347. ].join("|") + ")", "gi"),
  348. // image title ":{anything}:", etc.
  349. regexEmoji = /:(.*):/,
  350.  
  351. comments = $$(".js-discussion .timeline-comment-wrapper"),
  352. len = comments.length,
  353.  
  354. loop = () => {
  355. let wrapper, el, tmp, txt, img, hasLink, dupe;
  356. max = 0;
  357. while (max < 20 && indx < len) {
  358. if (indx >= len) {
  359. return;
  360. }
  361. wrapper = comments[indx];
  362. // save author list to prevent repeat +1s
  363. el = $(".timeline-comment-header .author", wrapper);
  364. txt = (el ? el.textContent || "" : "").toLowerCase();
  365. dupe = true;
  366. if (txt && authors.indexOf(txt) < 0) {
  367. authors[authors.length] = txt;
  368. dupe = false;
  369. }
  370. el = $(".comment-body", wrapper);
  371. // ignore quoted messages, but get all fragments
  372. tmp = $$(".email-fragment", el);
  373. // some posts only contain a link to related issues; these should not be counted as a +1
  374. // see https://github.com/isaacs/github/issues/618#issuecomment-200869630
  375. hasLink = $$(tmp.length ? ".email-fragment .issue-link" : ".issue-link", el).length;
  376. if (tmp.length) {
  377. // ignore quoted messages
  378. txt = getAllText(tmp);
  379. } else {
  380. txt = el.textContent.trim();
  381. }
  382. if (!txt) {
  383. img = $("img", el);
  384. if (img) {
  385. txt = img.getAttribute("title") || img.getAttribute("alt");
  386. }
  387. }
  388. // remove fluff
  389. txt = txt.replace(regexEmoji, "").replace(regexPlus, "").replace(regexHide, "").trim();
  390. if (txt === "" || (txt.length < 4 && !hasLink)) {
  391. if (settings.plus1.isHidden) {
  392. wrapper.classList.add("ghic-hidden");
  393. total++;
  394. // one +1 per author
  395. if (!dupe) {
  396. count++;
  397. }
  398. } else if (!init) {
  399. wrapper.classList.remove("ghic-hidden");
  400. }
  401. max++;
  402. }
  403. indx++;
  404. }
  405. if (indx < len) {
  406. setTimeout(() => {
  407. loop();
  408. }, 200);
  409. } else {
  410. $(".ghic-menu .ghic-plus1 .ghic-count").textContent = total ? "(" + total + ")" : " ";
  411. toggleClass($(".ghic-menu ." + settings.plus1.name), "ghic-has-content", total);
  412. addCountToReaction(count);
  413. }
  414. };
  415. loop();
  416. }
  417.  
  418. function getAllText(el) {
  419. let txt = "",
  420. indx = el.length;
  421. // text order doesn't matter
  422. while (indx--) {
  423. txt += el[indx].textContent.trim();
  424. }
  425. return txt;
  426. }
  427.  
  428. function addCountToReaction(count) {
  429. if (!count) {
  430. count = ($(".ghic-menu .ghic-plus1 .ghic-count").textContent || "")
  431. .replace(/[()]/g, "")
  432. .trim();
  433. }
  434. let comment = $(".timeline-comment"),
  435. tmp = $(
  436. ".has-reactions button[value='+1 react'], .has-reactions button[value='+1 unreact']",
  437. comment
  438. ),
  439. el = $(".ghic-count", comment);
  440. if (el) {
  441. // the count may have been appended to the comment & now
  442. // there is a reaction, so remove any "ghic-count" elements
  443. el.parentNode.removeChild(el);
  444. }
  445. if (count) {
  446. if (tmp) {
  447. el = document.createElement("span");
  448. el.className = "ghic-count";
  449. el.textContent = count ? " + " + count + " (from hidden comments)" : "";
  450. tmp.appendChild(el);
  451. } else {
  452. el = document.createElement("p");
  453. el.className = "ghic-count";
  454. el.innerHTML = "<hr>" + plus1Icon + " " + count + " (from hidden comments)";
  455. $(".comment-body", comment).appendChild(el);
  456. }
  457. }
  458. }
  459.  
  460. function hideParticipant(el) {
  461. let els, indx, len, name,
  462. results = [];
  463. if (el) {
  464. el.classList.toggle("comments-hidden");
  465. hide = el.classList.contains("comments-hidden");
  466. name = el.getAttribute("aria-label");
  467. els = $$(".js-discussion .author");
  468. len = els.length;
  469. for (indx = 0; indx < len; indx++) {
  470. if (els[indx].textContent.trim() === name) {
  471. results[results.length] = closest(
  472. ".timeline-comment-wrapper, .commit-comment, .discussion-item",
  473. els[indx]
  474. );
  475. }
  476. }
  477. // use a different participant class name to hide timeline events
  478. // or unselecting all users will show everything
  479. if (el.classList.contains("comments-hidden")) {
  480. addClass(results, "ghic-hidden-participant");
  481. } else {
  482. removeClass(results, "ghic-hidden-participant");
  483. }
  484. results = [];
  485. }
  486. }
  487.  
  488. function update() {
  489. busy = true;
  490. if ($("#discussion_bucket") && $(".ghic-button")) {
  491. let keys = Object.keys(settings),
  492. indx = keys.length;
  493. while (indx--) {
  494. // true flag for init - no need to remove classes
  495. hideStuff(keys[indx], true);
  496. }
  497. }
  498. busy = false;
  499. }
  500.  
  501. function checkItem(event) {
  502. busy = true;
  503. if (document.getElementById("discussion_bucket")) {
  504. let name,
  505. target = event.target,
  506. wrap = target && target.parentNode;
  507. if (target && wrap) {
  508. if (target.nodeName === "INPUT" && wrap.classList.contains("ghic-right")) {
  509. getInputValues();
  510. saveSettings();
  511. // extract ghic-{name}, because it matches the name in settings
  512. name = wrap.className.replace("ghic-right", "").replace("ghic-has-content", "").trim();
  513. if (wrap.classList.contains(name)) {
  514. hideStuff(name.replace("ghic-", ""));
  515. }
  516. } else if (target.classList.contains("ghic-avatar")) {
  517. // make sure we're targeting the span wrapping the image
  518. hideParticipant(target.nodeName === "IMG" ? target.parentNode : target);
  519. } else if (regex.test(target.nodeName)) {
  520. // clicking on the SVG may target the svg or path inside
  521. hideParticipant(closest(".ghic-avatar", target));
  522. }
  523. }
  524. }
  525. busy = false;
  526. }
  527.  
  528. function removeObservers() {
  529. observers.forEach(observer => {
  530. if (observer) {
  531. observer.disconnect();
  532. }
  533. });
  534. observers = [];
  535. }
  536.  
  537. function addObservers() {
  538. // DOM targets - to detect GitHub dynamic ajax page loading
  539. // Observe progressively loaded content
  540. $$(".js-discussion").forEach(target => {
  541. const obsrvr = new MutationObserver(mutations => {
  542. mutations.forEach(mutation => {
  543. // preform checks before adding code wrap to minimize function calls
  544. const tar = mutation.target;
  545. if (!busy && tar === target) {
  546. clearTimeout(debounce);
  547. debounce = setTimeout(() => {
  548. update();
  549. }, 500);
  550. }
  551. });
  552. });
  553. obsrvr.observe(target, {
  554. childList: true,
  555. subtree: true
  556. });
  557. observers.push(obsrvr);
  558. });
  559. }
  560.  
  561. function $(selector, el) {
  562. return (el || document).querySelector(selector);
  563. }
  564.  
  565. function $$(selector, el) {
  566. return Array.from((el || document).querySelectorAll(selector));
  567. }
  568.  
  569. function closest(selector, el) {
  570. while (el && el.nodeType === 1) {
  571. if (el.matches(selector)) {
  572. return el;
  573. }
  574. el = el.parentNode;
  575. }
  576. return null;
  577. }
  578.  
  579. function addClass(els, name) {
  580. let indx,
  581. len = els.length;
  582. for (indx = 0; indx < len; indx++) {
  583. els[indx].classList.add(name);
  584. }
  585. return len;
  586. }
  587.  
  588. function removeClass(els, name) {
  589. let indx,
  590. len = els.length;
  591. for (indx = 0; indx < len; indx++) {
  592. els[indx].classList.remove(name);
  593. }
  594. }
  595.  
  596. function toggleClass(els, name, flag) {
  597. els = Array.isArray(els) ? els : [els];
  598. let el,
  599. indx = els.length;
  600. while (indx--) {
  601. el = els[indx];
  602. if (el) {
  603. if (typeof flag === "undefined") {
  604. flag = !el.classList.contains(name);
  605. }
  606. if (flag) {
  607. el.classList.add(name);
  608. } else {
  609. el.classList.remove(name);
  610. }
  611. }
  612. }
  613. }
  614.  
  615. function init() {
  616. busy = true;
  617. removeObservers();
  618. getSettings();
  619. addMenu();
  620. $("body").addEventListener("input", checkItem);
  621. $("body").addEventListener("click", checkItem);
  622. update();
  623. busy = false;
  624. }
  625.  
  626. // update TOC when content changes
  627. document.addEventListener("pjax:end", addMenu);
  628. init();
  629.  
  630. })();