v2ex-reaciton

给v2ex增加emoji reaction功能

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);