Mastodonに引用ボタンを追加する

MastodonのDeck UIにtootを引用するためのURLコピーボタンをboostボタンの隣に追加します。

// SPDX-License-Identifier: MIT
// ==UserScript==
// @name      Mastodonに引用ボタンを追加する
// @name:en      Add button to copy toot's url
// @name:ja      Mastodonに引用ボタンを追加する
// @namespace    http://www.eniehack.net/~eniehack/works/firefox-userscripts
// @version      0.2.1
// @description:en  Add button to copy toot's url for quote toot on Mastodon's deck UI
// @description:ja  MastodonのDeck UIにtootを引用するためのURLコピーボタンをboostボタンの隣に追加します。
// @author       eniehack
// @license      MIT
// @match        https://fedibird.com/deck/*
// @match        https://best-friends.chat/deck/*
// @match        https://mstdn.jp/deck/*
// @match        https://pawoo.net/deck/*
// @grant        GM.setValue
// @grant        GM.getValue
// @description MastodonのDeck UIにtootを引用するためのURLコピーボタンをboostボタンの隣に追加します。
// ==/UserScript==
/*jshint esversion: 8 */

(async () => {
  const insertBefore = (newNode, existingNode) => {
    existingNode.parentNode.insertBefore(newNode, existingNode.previousSibling);
  };

  const createQuoteButtonElement = () => {
    const qtBtn = document.createElement("button");
    qtBtn.setAttribute(
      "class",
      "status__action-bar__button icon-button quote-icon",
    );
    qtBtn.setAttribute("type", "button");
    qtBtn.setAttribute(
      "style",
      "font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 18px;",
    );
    qtBtn.setAttribute("aria-label", "quote");
    qtBtn.setAttribute("aria-hidden", "false");
    qtBtn.setAttribute("title", "quote");
    qtBtn.setAttribute("tabindex", "0");
    const quoteIcon = document.createElement("i");
    quoteIcon.setAttribute("class", "fa fa-quote-left");
    quoteIcon.setAttribute("aria-hidden", "true");
    qtBtn.appendChild(quoteIcon);
    return qtBtn;
  };

  const fetchPostId = (target) => {
    return target
      .querySelector("a.status__relative-time")
      .href.match(/\/[0-9]+$/)[0]
      .substring(1);
  };

  const generateText = async (url) => {
    const tmpl = await GM.getValue("template", `{{url}}`);
    return tmpl.replace("{{url}}", url);
  };

  const copyText = (text) => {
    navigator.clipboard.writeText(text);
  };

  const insertQuoteButton = (targetArticle) => {
    const target = targetArticle.querySelector(
      ".status__action-bar__button.star-icon.icon-button",
    );
    const qtbtn = createQuoteButtonElement();
    // console.log(targetArticle)
    const postId = fetchPostId(targetArticle);
    const textareaElem = document.querySelector(
      "textarea.autosuggest-textarea__textarea",
    );
    qtbtn.onclick = () => {
      fetch(`https://${location.host}/api/v1/statuses/${postId}`)
        .then((res) => res.json())
        .then((json) => json.url)
        .then((url) => generateText(url))
        .then((txt) => copyText(txt));
    };
    insertBefore(qtbtn, target);
  };

  const callback = (entries, observer) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        insertQuoteButton(entry.target);
      } else {
        const quotebtn = entry.target.querySelector(".quote-icon");
        if (quotebtn === null) continue;
        quotebtn.remove();
      }
    }
  };

  const mutationObservers = [];

  setTimeout(
    () => {
      //console.debug(target);
      const mutationObserverTargets = document.querySelectorAll(
        "div.column > div.scrollable > div.item-list",
      );

      const io = new IntersectionObserver(callback);

      const mutationObserverConfig = {
        attributes: false,
        childList: true,
        subtree: false,
      };
      mutationObserverTargets.forEach((target) => {
        const mo = new MutationObserver((mutations, observer) => {
          for (const mutation of mutations) {
            if (0 < mutation.addedNodes.length) {
              for (const node of mutation.addedNodes) {
                if (node.tagName !== "ARTICLE") continue;
                io.observe(node);
              }
            }
            if (0 < mutation.removedNodes.length) {
              mutation.removedNodes.forEach((node) => {
                io.unobserve(node);
              });
            }
            //console.log(mutation);
          }
        });
        mo.observe(target, mutationObserverConfig);
        mutationObservers.push(mo);
      });

      //console.debug(articles.length);
      const articles = document.querySelectorAll("article");
      articles.forEach((article) => {
        io.observe(article);
      });
    },
    await GM.getValue("insert_before_second", 3000),
  );

  document.addEventListener("unload", () => {
    mutationObservers.forEach((mo) => mo.disconnect());
  });
})();