// ==UserScript==
// @name v2ex-reaciton
// @namespace npm/vite-plugin-monkey
// @version 0.1.1
// @author yuyinws
// @description 给v2ex增加emoji reaction功能
// @license MIT
// @icon https://vitejs.dev/logo.svg
// @iconURL https://www.v2ex.com/static/favicon.ico
// @match *://*.v2ex.com/t/*
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// ==/UserScript==
(o=>{const e=document.createElement("style");e.dataset.source="vite-plugin-monkey",e.textContent=o,document.head.append(e)})(' :root{--emojir-text-primary: #24292f}@media (prefers-color-scheme: dark){:root{--emojir-text-primary: #f5f5f5}}.emoji-reaction{display:flex;flex-direction:column;align-items:center;gap:1rem;margin:1rem 0;flex-wrap:wrap}.emoji-title{font-size:14px;font-weight:600;cursor:pointer;color:var(--emojir-text-primary)}.emoji-face-icon:before,.emoji-face-icon::-webkit-details-marker{display:none}.emoji-face-icon::marker{content:""}.emoji-face-icon{height:100%;width:100%;display:flex;justify-content:center;align-items:center;cursor:pointer}.emoji-list{display:flex;gap:5px}.emoji-menu{position:relative;background:#f6f8fa;border:1px solid #d0d7de;border-radius:50%;width:24px;height:24px}.emoji-menu:hover{background:#eaeef2}.emoji-panel-list{display:flex;flex-wrap:wrap;gap:.3rem}.emoji-panel{padding:.5rem;position:absolute;z-index:10;box-shadow:0 0 10px #0000001a;border-radius:.5rem;background-color:#fff;display:flex;flex-wrap:wrap;width:9.5rem}.emoji-panel-login{font-size:12px;color:#2563eb!important;font-style:italic}.emoji-item{padding:.5rem;width:1rem;height:1rem;cursor:pointer;border-radius:3px;display:flex;justify-content:center;align-items:center}.emoji-item:hover{background:#f3f4f6;font-size:20px;transition:font-size .2s ease-in-out}.emoji-item-reacted{background:#ddf4ff}.emoji-counter{padding:0 4px;font-size:12px;border-radius:100px;background:red;height:24px;width:34px;line-height:24px;background:#fff;border:1px solid #d1d5db;cursor:pointer;color:#222}.emoji-counter:hover{background:#eaeef2}.emoji-counter-reacted{background:#ddf4ff;border:1px solid #0969da}.emoji-counter-reacted:hover{background:#b6e3ff}.emoji-item-disabled{cursor:not-allowed;opacity:.5} ');
(function (vue) {
'use strict';
function getSearchParam(key) {
const params = new URLSearchParams(window.location.search);
return params.get(key);
}
const emojiMap = {
THUMBS_UP: "👍",
THUMBS_DOWN: "👎",
LAUGH: "😄",
HOORAY: "🎉",
CONFUSED: "😕",
HEART: "❤️",
ROCKET: "🚀",
EYES: "👀"
};
const serverDomin = "https://v2ex-reaction.vercel.app";
const token = vue.ref("");
const authURL = vue.ref("");
const isAuth = vue.ref(false);
function useAuth() {
async function genAuthURL() {
const href = window.location.href;
const response = await fetch(`${serverDomin}/authorize?app_return_url=${href}`);
const data = await response.text();
authURL.value = data;
}
function setToken() {
const emoji_token = getSearchParam("emoji-reaction-token") || localStorage.getItem("emoji-reaction-token");
if (emoji_token) {
localStorage.setItem("emoji-reaction-token", emoji_token);
token.value = emoji_token;
isAuth.value = true;
}
}
setToken();
return {
genAuthURL,
authURL,
token,
isAuth
};
}
const reactions = vue.ref([]);
const subjectId = vue.ref("");
const filteredReactions = vue.computed(() => {
return reactions.value.filter((reaction) => reaction.totalCount > 0);
});
const totalCount = vue.computed(() => {
return filteredReactions.value.reduce((total, reaction) => {
return total + reaction.totalCount;
}, 0);
});
function useReaction() {
const discussionUrl = vue.ref("");
const loading = vue.ref(false);
async function getReaction() {
try {
loading.value = true;
const pathname = window.location.pathname;
if (pathname.includes("review"))
return;
const token2 = localStorage.getItem("emoji-reaction-token");
const url = new URL(`${serverDomin}/getDiscussion`);
if (token2)
url.searchParams.append("token", token2);
if (pathname)
url.searchParams.append("pathname", pathname);
const response = await fetch(url.toString());
const { data, state } = await response.json();
if (state === "fail")
throw new Error(data);
const reactionNodes = data.search.nodes;
if (!reactionNodes.length) {
const createUrl = new URL(`${serverDomin}/createDiscussion`);
if (pathname)
createUrl.searchParams.append("pathname", pathname);
const res = await fetch(createUrl);
const createData = await res.json();
if (createData.state === "ok") {
setTimeout(() => {
getReaction();
}, 2e3);
}
} else {
const reactionGroups = reactionNodes[0].reactionGroups;
const discussionId = reactionNodes[0].id;
const _discussionUrl = reactionNodes[0].url;
subjectId.value = discussionId;
discussionUrl.value = _discussionUrl;
reactions.value = reactionGroups.map((reaction) => {
return {
content: reaction.content,
totalCount: reaction.users.totalCount,
viewerHasReacted: reaction.viewerHasReacted,
emoji: emojiMap[reaction.content]
};
});
}
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
const TOGGLE_REACTION_QUERY = (mode) => `
mutation($content: ReactionContent!, $subjectId: ID!) {
toggleReaction: ${mode}Reaction(input: {content: $content, subjectId: $subjectId}) {
reaction {
content
id
}
}
}`;
async function clickReaction(isAuth2, content, token2, viewerHasReacted, cb) {
try {
if (!isAuth2)
return;
await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token2}`
},
body: JSON.stringify({
query: TOGGLE_REACTION_QUERY(viewerHasReacted ? "remove" : "add"),
variables: {
subjectId: subjectId.value,
content
}
})
});
await getReaction();
} catch (error) {
console.log(error);
} finally {
cb();
}
}
return {
reactions,
getReaction,
filteredReactions,
totalCount,
clickReaction,
discussionUrl,
loading
};
}
const _hoisted_1$1 = { class: "emoji-list" };
const _hoisted_2$1 = { class: "emoji-face-icon" };
const _hoisted_3$1 = ["fill"];
const _hoisted_4$1 = /* @__PURE__ */ vue.createElementVNode("path", { d: "M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm3.82 1.636a.75.75 0 0 1 1.038.175l.007.009c.103.118.22.222.35.31.264.178.683.37 1.285.37.602 0 1.02-.192 1.285-.371.13-.088.247-.192.35-.31l.007-.008a.75.75 0 0 1 1.222.87l-.022-.015c.02.013.021.015.021.015v.001l-.001.002-.002.003-.005.007-.014.019a2.066 2.066 0 0 1-.184.213c-.16.166-.338.316-.53.445-.63.418-1.37.638-2.127.629-.946 0-1.652-.308-2.126-.63a3.331 3.331 0 0 1-.715-.657l-.014-.02-.005-.006-.002-.003v-.002h-.001l.613-.432-.614.43a.75.75 0 0 1 .183-1.044ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5.25 2.25.592.416a97.71 97.71 0 0 0-.592-.416Z" }, null, -1);
const _hoisted_5$1 = [
_hoisted_4$1
];
const _hoisted_6$1 = { class: "emoji-panel" };
const _hoisted_7$1 = ["href"];
const _hoisted_8$1 = /* @__PURE__ */ vue.createElementVNode("span", { style: { "font-size": "12px", "font-style": "italic", "color": "#94a3b8" } }, "以添加反应", -1);
const _hoisted_9 = { class: "emoji-panel-list" };
const _hoisted_10 = ["onClick"];
const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
__name: "Menu",
props: {
reactions: {
type: Array,
required: true
},
color: {
type: String,
default: "#000"
}
},
setup(__props) {
const { clickReaction } = useReaction();
const { token: token2, isAuth: isAuth2, authURL: authURL2 } = useAuth();
const emojiPanelRef = vue.ref(null);
const vClickOutside = {
beforeMount(el, binding) {
el.clickOutsideEvent = function(event) {
if (!(el === event.target || el.contains(event.target)))
binding.value(event);
};
document.addEventListener("mousedown", el.clickOutsideEvent);
},
beforeUnmount(el) {
document.removeEventListener("mousedown", el.clickOutsideEvent);
}
};
function handleClickOutside() {
emojiPanelRef.value.open = false;
}
return (_ctx, _cache) => {
return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [
vue.withDirectives((vue.openBlock(), vue.createElementBlock("details", {
ref_key: "emojiPanelRef",
ref: emojiPanelRef,
class: "emoji-menu"
}, [
vue.createElementVNode("summary", _hoisted_2$1, [
(vue.openBlock(), vue.createElementBlock("svg", {
"aria-hidden": "true",
focusable: "false",
role: "img",
viewBox: "0 0 16 16",
width: "16",
height: "16",
fill: __props.color,
style: { "display": "inline-block", "user-select": "none", "vertical-align": "text-bottom", "overflow": "visible" }
}, _hoisted_5$1, 8, _hoisted_3$1))
]),
vue.createElementVNode("div", _hoisted_6$1, [
!vue.unref(isAuth2) ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
vue.createElementVNode("a", {
href: vue.unref(authURL2),
class: "emoji-panel-login"
}, "登录", 8, _hoisted_7$1),
_hoisted_8$1
], 64)) : vue.createCommentVNode("", true),
vue.createElementVNode("div", _hoisted_9, [
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(__props.reactions, (item, index) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: index,
class: vue.normalizeClass([[
item.viewerHasReacted ? "emoji-item-reacted" : "",
vue.unref(isAuth2) ? "" : "emoji-item-disabled"
], "emoji-item"]),
onClick: ($event) => vue.unref(clickReaction)(vue.unref(isAuth2), item.content, vue.unref(token2), item.viewerHasReacted, handleClickOutside)
}, vue.toDisplayString(item.emoji), 11, _hoisted_10);
}), 128))
])
])
])), [
[vClickOutside, handleClickOutside]
])
]);
};
}
});
const _hoisted_1 = { key: 0 };
const _hoisted_2 = /* @__PURE__ */ vue.createElementVNode("img", {
width: "50",
style: { "margin-top": "1rem" },
height: "50",
src: "https://raw.githubusercontent.com/yuyinws/v2ex-reaction/main/source/loading.gif",
alt: "loading",
srcset: ""
}, null, -1);
const _hoisted_3 = [
_hoisted_2
];
const _hoisted_4 = { key: 1 };
const _hoisted_5 = { class: "emoji-reaction" };
const _hoisted_6 = ["href"];
const _hoisted_7 = { class: "emoji-list" };
const _hoisted_8 = ["onClick"];
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
__name: "App",
setup(__props) {
const {
reactions: reactions2,
getReaction,
filteredReactions: filteredReactions2,
totalCount: totalCount2,
clickReaction,
discussionUrl,
loading
} = useReaction();
const { genAuthURL, isAuth: isAuth2, token: token2 } = useAuth();
function init() {
if (!isAuth2.value)
genAuthURL();
getReaction();
}
vue.onMounted(() => {
init();
});
return (_ctx, _cache) => {
return vue.unref(loading) ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, _hoisted_3)) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_4, [
vue.createElementVNode("div", _hoisted_5, [
vue.createElementVNode("a", {
class: "emoji-title",
href: vue.unref(discussionUrl),
target: "_blank"
}, vue.toDisplayString(vue.unref(totalCount2)) + "个反应 ", 9, _hoisted_6),
vue.createElementVNode("div", _hoisted_7, [
vue.createVNode(_sfc_main$1, {
reactions: vue.unref(reactions2),
color: "#444"
}, null, 8, ["reactions"]),
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(filteredReactions2), (item, index) => {
return vue.openBlock(), vue.createElementBlock("div", {
key: index,
class: vue.normalizeClass([[
item.viewerHasReacted ? "emoji-counter-reacted" : "",
vue.unref(isAuth2) ? "" : "emoji-item-disabled"
], "emoji-counter"]),
onClick: ($event) => vue.unref(clickReaction)(vue.unref(isAuth2), item.content, vue.unref(token2), item.viewerHasReacted)
}, vue.toDisplayString(item.emoji) + " " + vue.toDisplayString(item.totalCount), 11, _hoisted_8);
}), 128))
])
])
]));
};
}
});
vue.createApp(_sfc_main).mount(
(() => {
const emojiApp = document.createElement("div");
emojiApp.id = "emoji-reaction";
const parentEL = document.querySelector("#Main > .box");
const topicBtnEl = document.querySelector(".topic_buttons");
parentEL.insertBefore(emojiApp, topicBtnEl);
return emojiApp;
})()
);
})(Vue);