🏠 Home 

Forward Slack messages, files, and Later items to channels and threads using an invisible link

This script enhances Slack's forwarding functionality and bypasses the built-in button's inability to forward to threads. It works for top-level and threaded messages, canvases, lists, files, and Later items which have no built-in way of copying links to them. Set which message or file 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 item. Right-click the rewind button to clear the set item.


Install this script?
// ==UserScript==
// @name        Forward Slack messages, files, and Later items to channels and threads using an invisible link
// @namespace   Violentmonkey Scripts
// @match       *://app.slack.com/client/*
// @grant       none
// @version     1.5
// @author      CyrilSLi
// @description This script enhances Slack's forwarding functionality and bypasses the built-in button's inability to forward to threads. It works for top-level and threaded messages, canvases, lists, files, and Later items which have no built-in way of copying links to them. Set which message or file 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 item. Right-click the rewind button to clear the set item.
// @license     MIT
// ==/UserScript==
function recurseClass(el, className) {
var parentEl = el.parentNode;
while (!parentEl.classList.contains(className)) {
parentEl = parentEl.parentNode;
if (parentEl == document.body) {
return null;
}
}
return parentEl;
}
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 style="width: 1em; height: 1em; --s: 18px; pointer-events: none;" data-s7u="true" data-qa="rewind" aria-hidden="true" viewBox="0 0 16 16" class="">
<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 style="width: 1em; height: 1em; --s: 18px; pointer-events: none;" data-s7u="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 = recurseClass(ev.target, "c-wysiwyg_container");
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, className) {
ev.preventDefault();
ev.stopPropagation();
function getLinkById() {
var fileId = recurseClass(ev.target, "c-virtual_list__item").id;
fwdLink = `https://files.slack.com/files-pri/${workspace}-${fileId}`;
}
const workspace = window.location.pathname.match(/(client\/)(T[0-9A-Z]+?)(\/)/)[2];
if (className == "p-saved_item__actions") {
var savedId = recurseClass(ev.target, "c-virtual_list__item").id;
if (savedId.startsWith("F")) {
fwdLink = `https://files.slack.com/files-pri/${workspace}-${savedId}`;
} else {
savedId = savedId.split("-");
fwdLink = `https://app.slack.com/archives/${savedId[0]}/p${savedId[1].split("_")[0].replace(".", "")}`;
}
} else if (className == "p-activity_ia4_page__item__actions") {
var activityId = recurseClass(ev.target, "c-virtual_list__item").id;
activityId = activityId.replace("thread_v2-", "");
if (activityId.includes("bot_dm_bundle-")) {
ev.target.style.display = "none";
fwdLink = "";
return;
} else {
activityId = activityId.split("-");
fwdLink = `https://app.slack.com/archives/${activityId[0]}/p${activityId[1].split("_")[0].replace(".", "")}`;
}
} else if (window.location.pathname.endsWith("/canvases") || window.location.pathname.endsWith("/lists") || window.location.pathname.endsWith("/files")) {
getLinkById();
} else {
var timestampEl = recurseClass(ev.target, "c-message_kit__actions");
if (timestampEl !== null) {
fwdLink = timestampEl.querySelector("a.c-timestamp").href;
} else {
getLinkById();
}
}
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();
}
}
});
["c-message_actions__group", "p-saved_item__actions", "p-activity_ia4_page__item__actions"].forEach((className) => {
if (document.getElementsByClassName(className)[0]) {
[...document.getElementsByClassName(className)].forEach((el) => {
if (!el.getElementsByClassName(reactBtnId)[0]) {
msgReact = reactBtn.cloneNode(true);
msgReact.addEventListener("click", (ev) => {getFwdLink(ev, className)});
el.insertBefore(msgReact, el.lastChild);
}
})
}
});
}
const observer = new MutationObserver((muts) => {
const primary = document.getElementsByClassName("p-view_contents--primary")[0];
const secondary = document.getElementsByClassName("p-view_contents--secondary")[0];
if ((primary && primary.innerHTML) || (secondary && secondary.innerHTML)) {
setTimeout(run, 0);
}
})
observer.observe(document.body, {
attributes: false,
childList: true,
characterData: false,
subtree:true
})