GitHub Sort Reactions

A userscript that sorts comments by reaction

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

QingJ © 2025

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