Forward Slack messages to channels and threads using an invisible link

Bypass the limitation of the native forwarding functionality not working in threads. Set which message to forward by clicking the fast forward icon beside it, then click the rewind button in an input area to insert an invisible link to that message. Right-click the rewind button to clear the set message.

当前为 2025-02-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Forward Slack messages to channels and threads using an invisible link
// @namespace   Violentmonkey Scripts
// @match       *://app.slack.com/client/*
// @grant       none
// @version     1.1
// @author      CyrilSLi
// @description Bypass the limitation of the native forwarding functionality not working in threads. Set which message to forward by clicking the fast forward icon beside it, then click the rewind button in an input area to insert an invisible link to that message. Right-click the rewind button to clear the set message.
// @license     MIT
// ==/UserScript==

const inputBtnId = "userscriptForwardMsgInput";
const inputSpanId = "userscriptForwardMsgInputSpan";
const reactBtnId = "userscriptForwardMsgReact";
const tooltip = "Link Forward";
var fwdLink = "";

const inputDivider = document.createElement("span");
inputDivider.className = inputSpanId + " c-wysiwyg_container__footer_divider";

const inputBtn = document.createElement("button");
inputBtn.className = inputBtnId + " c-button-unstyled c-icon_button c-icon_button--size_small c-wysiwyg_container__button c-wysiwyg_container__button--workflows c-icon_button--default";
inputBtn.setAttribute("tabindex", "0");
inputBtn.setAttribute("aria-label", tooltip);
inputBtn.setAttribute("delay", "500");
inputBtn.setAttribute("type", "button");
inputBtn.setAttribute("title", tooltip);
inputBtn.innerHTML = `
    <svg data-ghv="true" data-qa="rewind" aria-hidden="true" viewBox="0 0 16 16" class="" style="--s: 18px;">
        <path d="M9.196 8 15 4.633v6.734zm-.792-.696a.802.802 0 0 0 0 1.392l6.363 3.692c.52.302 1.233-.043 1.233-.696V4.308c0-.653-.713-.998-1.233-.696z"/>
        <path d="M1.196 8 7 4.633v6.734zm-.792-.696a.802.802 0 0 0 0 1.392l6.363 3.692c.52.302 1.233-.043 1.233-.696V4.308c0-.653-.713-.998-1.233-.696z"/>
    </svg>
`;

const reactBtn = document.createElement("button");
reactBtn.className = reactBtnId + " c-button-unstyled c-icon_button c-icon_button--size_small c-message_actions__button c-icon_button--default";
reactBtn.setAttribute("aria-label", tooltip);
reactBtn.setAttribute("type", "button");
reactBtn.setAttribute("title", tooltip);
reactBtn.innerHTML = `
    <svg data-ghv="true" data-qa="forward" aria-hidden="true" viewBox="0 0 16 16" class="">
        <path d="M6.804 8 1 4.633v6.734zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696z"/>
        <path d="M14.804 8 9 4.633v6.734zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C8.713 12.69 8 12.345 8 11.692V4.308c0-.653.713-.998 1.233-.696z"/>
    </svg>
`;

function setFwdLink(ev) {
    var parentEl = ev.target.parentNode;
    while (!parentEl.classList.contains("c-wysiwyg_container")) {
        parentEl = parentEl.parentNode;
    }
    var firstLine = [...document.getElementsByClassName("ql-editor")].filter(el => parentEl.contains(el))[0].children[0];
    const linkHTML = `<a href="${fwdLink}" rel="noopener noreferrer" target="_blank">&NoBreak;</a>`;
    firstLine.innerHTML = firstLine.innerHTML === "<br>" ? linkHTML : linkHTML + firstLine.innerHTML;
}

function clearFwdLink(ev) {
    ev.preventDefault();
    fwdLink = "";
    run();
    return false;
}

`
function getFwdLink(ev) {
    var parentEl = ev.target.parentNode;
    while (!parentEl.classList.contains("c-virtual_list__item")) {
        parentEl = parentEl.parentNode;
    }
    parentEl = parentEl.id;
    fwdLink = "https://app.slack.com/archives/";
    if (parentEl.includes("thread")) {
        parentEl = parentEl.split("-");
        fwdLink += parentEl[0];
        fwdLink += "/p" + parentEl.pop().split("_")[1].replace(".", "");
        fwdLink += "?thread_ts=" + parentEl[1];
        fwdLink += "&cid=" + parentEl[0];
    } else {
        fwdLink += window.location.href.split("?")[0].split("/").slice(-1)[0];
        fwdLink += "/p" + parentEl.replace(".", "");
    }
    run();
}
`

function getFwdLink(ev) {
    var parentEl = ev.target.parentNode;
    while (!parentEl.classList.contains("c-message_kit__actions")) {
        parentEl = parentEl.parentNode;
    }
    fwdLink = parentEl.querySelector("a.c-timestamp").href
    run();
}

function run() {
    [...document.getElementsByClassName("c-texty_buttons")].forEach((inputRow) => {
        if (fwdLink) {
            if (!inputRow.getElementsByClassName(inputBtnId)[0]) {
                inputRow.appendChild(inputDivider.cloneNode(true));
                msgInput = inputBtn.cloneNode(true);
                msgInput.addEventListener("click", setFwdLink);
                msgInput.addEventListener("contextmenu", clearFwdLink, false);
                inputRow.appendChild(msgInput);
            }
        } else {
            if (inputRow.getElementsByClassName(inputBtnId)[0]) {
                inputRow.getElementsByClassName(inputBtnId)[0].remove();
                inputRow.getElementsByClassName(inputSpanId)[0].remove();
            }
        }
    });
    if (document.getElementsByClassName("c-message_actions__group")[0]) {
        [...document.getElementsByClassName("c-message_actions__group")].forEach((el) => {
            if (!el.getElementsByClassName(reactBtnId)[0]) {
                const lastEl = el.querySelector("[data-qa='more_message_actions']");
                msgReact = reactBtn.cloneNode(true);
                msgReact.addEventListener("click", getFwdLink);
                el.insertBefore(msgReact, lastEl);
            }
        });
    }
}

const observer = new MutationObserver((muts) => {
    const view = document.getElementsByClassName("p-view_contents--primary")[0]
    if (view && view.innerHTML) {
        setTimeout(run, 0);
    }
})
observer.observe(document.body, {
    attributes: false,
    childList: true,
    characterData: false,
    subtree:true
})