您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A userscript that sorts comments by reaction
- // ==UserScript==
- // @name GitHub Sort Reactions
- // @version 0.2.17
- // @description A userscript that sorts comments by reaction
- // @license MIT
- // @author Rob Garrison
- // @namespace https://github.com/Mottie
- // @match https://github.com/*
- // @run-at document-idle
- // @grant GM_addStyle
- // @grant GM_getValue
- // @grant GM_setValue
- // @require https://gf.qytechs.cn/scripts/28721-mutations/code/mutations.js?version=1108163
- // @icon https://github.githubassets.com/pinned-octocat.svg
- // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
- // ==/UserScript==
- (() => {
- "use strict";
- const nonInteger = /[^\d]/g;
- const reactionValues = {
- "THUMBS_UP": 1,
- "HOORAY": 1,
- "HEART": 1,
- "LAUGH": 0.5,
- "CONFUSED": -0.5,
- "THUMBS_DOWN": -1
- };
- const currentSort = {
- init: false,
- el: null,
- dir: 0, // 0 = unsorted, 1 = desc, 2 = asc
- busy: false,
- type: GM_getValue("selected-reaction", "NONE")
- };
- const emojiSrc = "https://github.githubassets.com/images/icons/emoji/unicode";
- const sortBlock = `
- <div class="TimelineItem ghsr-sort-block ghsr-is-collapsed js-timeline-progressive-focus-container">
- <div class="avatar-parent-child TimelineItem-avatar border ghsr-sort-avatar ghsr-no-selection">
- <div class="ghsr-icon-wrap tooltipped tooltipped-n" aria-label="Click to toggle reaction sort menu">
- <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">
- <path d="M15 8 1 8 8 0zM15 9 1 9 8 16z"/>
- </svg>
- </div>
- <g-emoji></g-emoji>
- <button class="ghsr-sort-button ghsr-avatar-sort btn btn-sm tooltipped tooltipped-n" aria-label="Toggle selected reaction sort direction">
- <span></span>
- </button>
- </div>
- <div class="timeline-comment ml-n3">
- <div class="timeline-comment-header comment comment-body">
- <h3 class="timeline-comment-header-text f5 text-normal">
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by +1 reaction" data-sort="THUMBS_UP">
- <g-emoji alias="+1" class="emoji" fallback-src="${emojiSrc}/1f44d.png">👍</g-emoji>
- </button>
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by -1 reaction" data-sort="THUMBS_DOWN">
- <g-emoji alias="-1" class="emoji" fallback-src="${emojiSrc}/1f44e.png">👎</g-emoji>
- </button>
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by laugh reaction" data-sort="LAUGH">
- <g-emoji alias="smile" class="emoji" fallback-src="${emojiSrc}/1f604.png">😄</g-emoji>
- </button>
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by hooray reaction" data-sort="HOORAY">
- <g-emoji alias="tada" class="emoji" fallback-src="${emojiSrc}/1f389.png">🎉</g-emoji>
- </button>
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by confused reaction" data-sort="CONFUSED">
- <g-emoji alias="thinking_face" class="emoji" fallback-src="${emojiSrc}/1f615.png">😕</g-emoji>
- </button>
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n" type="button" aria-label="Sort by heart reaction" data-sort="HEART">
- <g-emoji alias="heart" class="emoji" fallback-src="${emojiSrc}/2764.png">❤️</g-emoji>
- </button>
- <button class="ghsr-sort-button btn btn-sm tooltipped tooltipped-n tooltipped-multiline" type="button" aria-label="Sort by reaction evaluation
- (thumbs up, hooray & heart = +1;
- laugh = +0.5; confused = -0.5;
- thumbs down = -1)" data-sort="ACTIVE">
- <g-emoji alias="speak_no_evil" class="emoji" fallback-src="${emojiSrc}/1f64a.png">🙊</g-emoji>
- </button>
- </h3>
- </div>
- </div>
- </div>`;
- function sumOfReactions(el) {
- return Object.keys(reactionValues).reduce((sum, item) => {
- const elm = $(`.comment-reactions-options button[value*="${item}"]`, el);
- return sum + (getValue(elm) * reactionValues[item]);
- }, 0);
- }
- function getValue(elm) {
- return elm ?
- parseInt(elm.textContent.replace(nonInteger, "") || "0", 10) :
- 0;
- }
- function extractSortValue(elm, type, dir) {
- if (dir === 0 || type === "NONE" || type === "ACTIVE") {
- return parseFloat(
- elm.dataset[`sortComment${dir === 0 ? "Date" : "Sum"}`]
- );
- }
- return getValue($(`.comment-reactions button[value*="${type}"]`, elm));
- }
- function stableSortValue(elm) {
- return parseInt(elm.dataset.sortCommentDate, 10);
- }
- function updateAvatar() {
- GM_setValue("selected-reaction", currentSort.type);
- const block = $(".ghsr-sort-block"),
- avatar = $(".ghsr-sort-avatar", block),
- icon = $(".ghsr-sort-button span", avatar);
- if (avatar) {
- let current = $(`.comment-body [data-sort=${currentSort.type}]`, block);
- avatar.classList.remove("ghsr-no-selection");
- avatar.replaceChild(
- $("g-emoji", current).cloneNode(true),
- $("g-emoji", avatar)
- );
- if (currentSort.dir === 0) {
- // use unsorted svg in sort button
- current = $(".ghsr-sort-icon", avatar).cloneNode(true);
- current.classList.remove("ghsr-sort-icon");
- icon.textContent = "";
- icon.appendChild(current);
- } else {
- icon.textContent = currentSort.dir !== 1 ? "▲" : "▼";
- }
- }
- }
- function sort() {
- currentSort.busy = true;
- const fragment = document.createDocumentFragment(),
- container = $(".js-discussion"),
- sortBlock = $(".ghsr-sort-block"),
- loadMore = $("#progressive-timeline-item-container"),
- dir = currentSort.dir,
- sortAsc = dir !== 1,
- type = currentSort.el ? currentSort.el.dataset.sort : "NONE";
- currentSort.type = type;
- updateAvatar();
- $$(".js-timeline-item")
- .sort((a, b) => {
- const av = extractSortValue(a, type, dir),
- bv = extractSortValue(b, type, dir);
- if (av === bv) {
- return stableSortValue(a) - stableSortValue(b);
- }
- return sortAsc ? av - bv : bv - av;
- })
- .forEach(el => {
- fragment.appendChild(el);
- });
- container.appendChild(fragment);
- if (loadMore) {
- // Move load more comments to top
- sortBlock.parentNode.insertBefore(loadMore, sortBlock.nextSibling);
- }
- setTimeout(() => {
- currentSort.busy = false;
- }, 100);
- }
- function update() {
- if (!currentSort.init || $$(".has-reactions").length < 2) {
- return toggleSortBlock(false);
- }
- toggleSortBlock(true);
- const items = $$(".js-timeline-item:not([data-sort-comment-date])");
- if (items) {
- items.forEach(el => {
- let date = $("[datetime]", el);
- if (date) {
- date = date.getAttribute("datetime");
- el.setAttribute("data-sort-comment-date", Date.parse(date));
- }
- // Add reset date & most active summation
- el.setAttribute("data-sort-comment-sum", sumOfReactions(el));
- });
- }
- if (currentSort.el && !currentSort.busy) {
- sort();
- }
- }
- function initSort(event) {
- let direction,
- target = event.target;
- if (target.classList.contains("ghsr-sort-button")) {
- event.preventDefault();
- event.stopPropagation();
- if (target.classList.contains("ghsr-avatar-sort")) {
- // Using avatar sort button; retarget button
- target = $(`.ghsr-sort-button[data-sort="${currentSort.type}"]`);
- currentSort.el = target;
- }
- $$(".ghsr-sort-button").forEach(el => {
- el.classList.toggle("selected", el === target);
- el.classList.remove("asc", "desc");
- });
- if (currentSort.el === target) {
- currentSort.dir = (currentSort.dir + 1) % 3;
- } else {
- currentSort.el = target;
- currentSort.dir = 1;
- }
- if (currentSort.dir !== 0) {
- direction = currentSort.dir === 1 ? "desc" : "asc";
- currentSort.el.classList.add(direction);
- $(".ghsr-avatar-sort").classList.add(direction);
- }
- sort();
- } else if (target.matches(".ghsr-sort-avatar, .ghsr-icon-wrap")) {
- $(".ghsr-sort-block").classList.toggle("ghsr-is-collapsed");
- }
- }
- function toggleSortBlock(show) {
- const block = $(".ghsr-sort-block");
- if (block) {
- block.style.display = show ? "block" : "none";
- } else if (show) {
- addSortBlock();
- }
- }
- function addSortBlock() {
- currentSort.busy = true;
- const first = $(".TimelineItem");
- if (first) {
- first.classList.add("ghsr-skip-sort");
- first.insertAdjacentHTML("afterEnd", sortBlock);
- }
- currentSort.busy = false;
- }
- function init() {
- if (!currentSort.init) {
- GM_addStyle(`
- .ghsr-sort-block .comment-body { padding: 0 10px; }
- .ghsr-sort-block .timeline-comment-header { position: relative; }
- .ghsr-sort-block .emoji { vertical-align: baseline; pointer-events: none; }
- .ghsr-sort-block .btn.asc .emoji:after { content: "▲"; }
- .ghsr-sort-block .btn.desc .emoji:after { content: "▼"; }
- .ghsr-sort-avatar, .ghsr-icon-wrap { height: 48px; width: 44px; text-align: center; }
- .ghsr-sort-avatar { background: rgba(128, 128, 128, 0.2); border: #777 1px solid; }
- .ghsr-sort-avatar .emoji { position: relative; top: -36px; }
- .ghsr-sort-avatar svg { pointer-events: none; }
- .ghsr-sort-avatar.ghsr-no-selection { cursor: pointer; padding: 0 4px 0 0; }
- .ghsr-sort-avatar.ghsr-no-selection .emoji,
- .ghsr-sort-avatar.ghsr-no-selection .btn,
- .ghsr-sort-avatar:not(.ghsr-no-selection) svg.ghsr-sort-icon { display: none; }
- .ghsr-sort-avatar .btn { border-radius: 20px; width: 20px; height: 20px; position: absolute; bottom: -5px; right: -5px; }
- .ghsr-sort-avatar .btn span { position: absolute; left: 5px; top: 0; pointer-events: none; }
- .ghsr-sort-avatar .btn.asc span { top: -3px; }
- .ghsr-sort-avatar .btn span svg { height: 10px; width: 10px; vertical-align: unset; }
- .ghsr-sort-block.ghsr-is-collapsed h3,
- .ghsr-sort-block.ghsr-is-collapsed .timeline-comment:before,
- .ghsr-sort-block.ghsr-is-collapsed .timeline-comment:after { display: none; }
- .ghsr-sort-block.ghsr-is-collapsed .timeline-comment { margin: 10px 0; }
- .ghsr-sort-block.ghsr-is-collapsed .TimelineItem-avatar { top: 6px; }
- `);
- document.addEventListener("ghmo:container", update);
- document.addEventListener("ghmo:comments", update);
- document.addEventListener("click", initSort);
- currentSort.init = true;
- update();
- // "NONE" can only be seen on userscript init/factory reset
- if ($(".ghsr-sort-block") && currentSort.type !== "NONE") {
- updateAvatar();
- }
- }
- }
- function $(selector, el) {
- return (el || document).querySelector(selector);
- }
- function $$(selector, el) {
- return [...(el || document).querySelectorAll(selector)];
- }
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", update, {once: true});
- } else {
- init();
- }
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址