GitHub Sort Reactions

A userscript that sorts comments by reaction

当前为 2018-05-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Sort Reactions
  3. // @version 0.2.4
  4. // @description A userscript that sorts comments by reaction
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @require https://gf.qytechs.cn/scripts/28721-mutations/code/mutations.js?version=264157
  14. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  15. // ==/UserScript==
  16. (() => {
  17. "use strict";
  18.  
  19. const nonInteger = /[^\d]/g,
  20. reactionValues = {
  21. "THUMBS_UP": 1,
  22. "HOORAY": 1,
  23. "HEART": 1,
  24. "LAUGH": 0.5,
  25. "CONFUSED": -0.5,
  26. "THUMBS_DOWN": -1
  27. },
  28. currentSort = {
  29. init: false,
  30. el: null,
  31. dir: 0, // 0 = unsorted, 1 = desc, 2 = asc
  32. busy: false,
  33. type: GM_getValue("selected-reaction", "NONE")
  34. },
  35.  
  36. sortBlock = `
  37. <div class="timeline-comment-wrapper ghsc-sort-block ghsc-is-collapsed">
  38. <div class="timeline-comment">
  39. <div class="avatar-parent-child timeline-comment-avatar border ghsc-sort-avatar ghsc-no-selection">
  40. <div class="ghsc-icon-wrap tooltipped tooltipped-n" aria-label="Click to toggle reaction sort menu">
  41. <svg aria-hidden="true" class="octicon ghsc-sort-icon" xmlns="http://www.w3.org/2000/svg" width="25" height="40" viewBox="0 0 16 16">
  42. <path d="M15 8 1 8 8 0zM15 9 1 9 8 16z"/>
  43. </svg>
  44. </div>
  45. <g-emoji></g-emoji>
  46. <button class="ghsc-sort-button ghsc-avatar-sort btn btn-sm tooltipped tooltipped-n" aria-label="Toggle selected reaction sort direction">
  47. <span></span>
  48. </button>
  49. </div>
  50. <div class="timeline-comment-header comment comment-body">
  51. <h3 class="timeline-comment-header-text f5 text-normal">
  52. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by +1 reaction" data-sort="THUMBS_UP">
  53. <g-emoji alias="+1" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png">?</g-emoji>
  54. </button>
  55. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by -1 reaction" data-sort="THUMBS_DOWN">
  56. <g-emoji alias="-1" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f44e.png">?</g-emoji>
  57. </button>
  58. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by laugh reaction" data-sort="LAUGH">
  59. <g-emoji alias="smile" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f604.png">?</g-emoji>
  60. </button>
  61. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by hooray reaction" data-sort="HOORAY">
  62. <g-emoji alias="tada" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f389.png">?</g-emoji>
  63. </button>
  64. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by confused reaction" data-sort="CONFUSED">
  65. <g-emoji alias="thinking_face" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f615.png">?</g-emoji>
  66. </button>
  67. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by heart reaction" data-sort="HEART">
  68. <g-emoji alias="heart" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/2764.png">❤️</g-emoji>
  69. </button>
  70. <button class="ghsc-sort-button btn btn-sm tooltipped tooltipped-n tooltipped-multiline" type="button" aria-label="Sort by reaction evaluation
  71. (thumbs up, hooray & heart = +1;
  72. laugh = +0.5; confused = -0.5;
  73. thumbs down = -1)" data-sort="ACTIVE">
  74. <g-emoji alias="speak_no_evil" class="emoji" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f64a.png">?</g-emoji>
  75. </button>
  76. </h3>
  77. </div>
  78. </div>
  79. </div>`;
  80.  
  81. function sumOfReactions(el) {
  82. return Object.keys(reactionValues).reduce((sum, item) => {
  83. const elm = $(`.comment-reactions-options button[value*="${item}"]`, el);
  84. return sum + (getValue(elm) * reactionValues[item]);
  85. }, 0);
  86. }
  87.  
  88. function getValue(elm) {
  89. return elm ?
  90. parseInt(elm.textContent.replace(nonInteger, "") || "0", 10) :
  91. 0;
  92. }
  93.  
  94. function extractSortValue(elm, type, dir) {
  95. if (dir === 0 || type === "NONE" || type === "ACTIVE") {
  96. return parseFloat(
  97. elm.dataset[`sortComment${dir === 0 ? "Date" : "Sum"}`]
  98. );
  99. }
  100. return getValue($(`.comment-reactions button[value*="${type}"]`, elm));
  101. }
  102.  
  103. function stableSortValue(elm) {
  104. return parseInt(elm.dataset.sortCommentDate, 10);
  105. }
  106.  
  107. function updateAvatar() {
  108. GM_setValue("selected-reaction", currentSort.type);
  109. const block = $(".ghsc-sort-block"),
  110. avatar = $(".ghsc-sort-avatar", block),
  111. icon = $(".ghsc-sort-button span", avatar);
  112. if (avatar) {
  113. let current = $(`.comment-body [data-sort=${currentSort.type}]`, block);
  114. avatar.classList.remove("ghsc-no-selection");
  115. avatar.replaceChild(
  116. $("g-emoji", current).cloneNode(true),
  117. $("g-emoji", avatar)
  118. );
  119. if (currentSort.dir === 0) {
  120. // use unsorted svg in sort button
  121. current = $(".ghsc-sort-icon", avatar).cloneNode(true);
  122. current.classList.remove("ghsc-sort-icon");
  123. icon.textContent = "";
  124. icon.appendChild(current);
  125. } else {
  126. icon.textContent = currentSort.dir !== 1 ? "▲" : "▼";
  127. }
  128. }
  129. }
  130.  
  131. function sort() {
  132. currentSort.busy = true;
  133. const fragment = document.createDocumentFragment(),
  134. container = $(".js-discussion"),
  135. sortBlock = $(".ghsc-sort-block"),
  136. loadMore = $("#progressive-timeline-item-container"),
  137. dir = currentSort.dir,
  138. sortAsc = dir !== 1,
  139. type = currentSort.el ? currentSort.el.dataset.sort : "NONE";
  140. currentSort.type = type;
  141. updateAvatar();
  142.  
  143. $$(".js-timeline-item")
  144. .sort((a, b) => {
  145. const av = extractSortValue(a, type, dir),
  146. bv = extractSortValue(b, type, dir);
  147. if (av === bv) {
  148. return stableSortValue(a) - stableSortValue(b);
  149. }
  150. return sortAsc ? av - bv : bv - av;
  151. })
  152. .forEach(el => {
  153. fragment.appendChild(el);
  154. });
  155. container.appendChild(fragment);
  156. if (loadMore) {
  157. // Move load more comments to top
  158. sortBlock.parentNode.insertBefore(loadMore, sortBlock.nextSibling);
  159. }
  160. setTimeout(() => {
  161. currentSort.busy = false;
  162. }, 100);
  163. }
  164.  
  165. function update() {
  166. if (!currentSort.init || $$(".has-reactions").length < 2) {
  167. return toggleSortBlock(false);
  168. }
  169. toggleSortBlock(true);
  170. const items = $$(".js-timeline-item:not([data-sort-comment-date])");
  171. if (items) {
  172. items.forEach(el => {
  173. let date = $("[datetime]", el);
  174. if (date) {
  175. date = date.getAttribute("datetime");
  176. el.setAttribute("data-sort-comment-date", Date.parse(date));
  177. }
  178. // Add reset date & most active summation
  179. el.setAttribute("data-sort-comment-sum", sumOfReactions(el));
  180. });
  181. }
  182. if (currentSort.el && !currentSort.busy) {
  183. sort();
  184. }
  185. }
  186.  
  187. function initSort(event) {
  188. let direction,
  189. target = event.target;
  190. if (target.classList.contains("ghsc-sort-button")) {
  191. event.preventDefault();
  192. event.stopPropagation();
  193. if (target.classList.contains("ghsc-avatar-sort")) {
  194. // Using avatar sort button; retarget button
  195. target = $(`.ghsc-sort-button[data-sort="${currentSort.type}"]`);
  196. currentSort.el = target;
  197. }
  198. $$(".ghsc-sort-button").forEach(el => {
  199. el.classList.toggle("selected", el === target);
  200. el.classList.remove("asc", "desc");
  201. });
  202. if (currentSort.el === target) {
  203. currentSort.dir = (currentSort.dir + 1) % 3;
  204. } else {
  205. currentSort.el = target;
  206. currentSort.dir = 1;
  207. }
  208. if (currentSort.dir !== 0) {
  209. direction = currentSort.dir === 1 ? "desc" : "asc";
  210. currentSort.el.classList.add(direction);
  211. $(".ghsc-avatar-sort").classList.add(direction);
  212. }
  213. sort();
  214. } else if (target.matches(".ghsc-sort-avatar, .ghsc-icon-wrap")) {
  215. $(".ghsc-sort-block").classList.toggle("ghsc-is-collapsed");
  216. }
  217. }
  218.  
  219. function toggleSortBlock(show) {
  220. const block = $(".ghsc-sort-block");
  221. if (block) {
  222. block.style.display = show ? "block" : "none";
  223. } else if (show) {
  224. addSortBlock();
  225. }
  226. }
  227.  
  228. function addSortBlock() {
  229. currentSort.busy = true;
  230. const first = $(".timeline-comment-wrapper");
  231. first.classList.add("ghsc-skip-sort");
  232. first.insertAdjacentHTML("afterEnd", sortBlock);
  233. currentSort.busy = false;
  234. }
  235.  
  236. function init() {
  237. if (!currentSort.init) {
  238. GM_addStyle(`
  239. .ghsc-sort-block .comment-body { padding: 0 10px; }
  240. .ghsc-sort-block .emoji { vertical-align: baseline; pointer-events: none; }
  241. .ghsc-sort-block .btn.asc .emoji:after { content: "▲"; }
  242. .ghsc-sort-block .btn.desc .emoji:after { content: "▼"; }
  243. .ghsc-sort-avatar, .ghsc-icon-wrap { height: 44px; width: 44px; text-align: center; }
  244. .ghsc-sort-avatar { background: rgba(128, 128, 128, 0.2); border: #777 1px solid; }
  245. .ghsc-sort-avatar .emoji { position: relative; top: -36px; }
  246. .ghsc-sort-avatar svg { pointer-events: none; }
  247. .ghsc-sort-avatar.ghsc-no-selection { cursor: pointer; padding: 0 4px 0 0; }
  248. .ghsc-sort-avatar.ghsc-no-selection .emoji,
  249. .ghsc-sort-avatar.ghsc-no-selection .btn,
  250. .ghsc-sort-avatar:not(.ghsc-no-selection) svg.ghsc-sort-icon { display: none; }
  251. .ghsc-sort-avatar .btn { border-radius: 20px; width: 20px; height: 20px; position: absolute; bottom: -5px; right: -5px; }
  252. .ghsc-sort-avatar .btn span { position: absolute; left: 5px; top: 0; pointer-events: none; }
  253. .ghsc-sort-avatar .btn.asc span { top: -3px; }
  254. .ghsc-sort-avatar .btn span svg { height: 10px; width: 10px; vertical-align: unset; }
  255. .ghsc-sort-block.ghsc-is-collapsed h3,
  256. .ghsc-sort-block.ghsc-is-collapsed .timeline-comment:before,
  257. .ghsc-sort-block.ghsc-is-collapsed .timeline-comment:after { display: none; }
  258. .ghsc-sort-block.ghsc-is-collapsed .timeline-comment { margin: 10px 0; }
  259. .ghsc-sort-block.ghsc-is-collapsed .timeline-comment-avatar { top: -22px; }
  260. `);
  261. document.addEventListener("ghmo:container", update);
  262. document.addEventListener("ghmo:comments", update);
  263. document.addEventListener("click", initSort);
  264. currentSort.init = true;
  265. update();
  266. // "NONE" can only be seen on userscript init/factory reset
  267. if ($(".ghsc-sort-block") && currentSort.type !== "NONE") {
  268. updateAvatar();
  269. }
  270. }
  271. }
  272.  
  273. function $(selector, el) {
  274. return (el || document).querySelector(selector);
  275. }
  276.  
  277. function $$(selector, el) {
  278. return [...(el || document).querySelectorAll(selector)];
  279. }
  280.  
  281. if (document.readyState === "loading") {
  282. document.addEventListener("DOMContentLoaded", update, {once: true});
  283. } else {
  284. init();
  285. }
  286. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址