Add a button to send a slack message to org-protocol
// ==UserScript== // @name Slack: Org Protocol Capture // @namespace Violentmonkey Scripts // @match https://app.slack.com/client/* // @grant none // @version 1.1 // @author srijan // @supportURL https://github.com/srijan/slack_org_protocol_capture // @description Add a button to send a slack message to org-protocol // @license MIT // ==/UserScript== var observing = false; // Function to open URIs in either a web browser or in Slack Desktop. function crossPlatformOpen(uri) { // If we're in an electron environment, use open(); if (isDesktop && isDesktop()) { console.log("[SLACK-ORG-PROTOCOL] Detected Slack desktop, opening with open"); open(uri); } else if (location && typeof location.href != 'undefined') { console.log("[SLACK-ORG-PROTOCOL] Opening with location.href"); location.href = uri; } else if (window && typeof window.open != 'undefined') { console.log("[SLACK-ORG-PROTOCOL] Opening with window.open"); window.open(uri, '_blank'); } else { alert("[SLACK-ORG-PROTOCOL] Cannot find a suitable API to open a URI (" + uri + "). This is a bug."); } }; // Function to create and insert the button function insertButton() { // Check if the button already exists to avoid duplicates if (isButtonPresent()) { console.debug("btn already present so skipping"); return; } // Create the new button element const newButton = document.createElement("button"); // Set the button classes newButton.classList.add( "c-button-unstyled", "c-icon_button", "c-icon_button--size_small", "c-message_actions__button", "c-icon_button--default", "custom-org-capture-button" ); // Set the SVG content inside the button newButton.innerHTML = ` <svg height="18" width="18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 58 58" xml:space="preserve"> <g> <path fill="currentColor" d="M36.293,12.121c0.391,0.391,1.023,0.391,1.414,0s0.391-1.023,0-1.414l-7.999-7.999c-0.001-0.001-0.001-0.001-0.002-0.002 L29,2l-0.706,0.706c-0.001,0.001-0.001,0.001-0.002,0.002l-7.999,7.999c-0.391,0.391-0.391,1.023,0,1.414s1.023,0.391,1.414,0 L28,5.828v27.586c0,0.552,0.447,1,1,1s1-0.448,1-1V5.828L36.293,12.121z"/> <path fill="currentColor" d="M57.981,32.676L57.53,32h-0.009l-3.583-5.381l-2.421-3.628l0.004-0.002L47.535,17H36v2h10.465l8.679,13H39v7H19v-7H2.856 l8.679-13H22v-2H10.465L0.431,32.031l-0.014,0.001L0,32.655v0.206v0.396v19.359C0,54.482,1.519,56,3.385,56h51.23 C56.481,56,58,54.482,58,52.616V33.239v-0.429l-0.014-0.036L57.981,32.676z"/> </g> </svg>`; // Set the onClick behavior newButton.onclick = async () => { const [sender, message, link] = getCurrentMessageDetails(); console.debug(sender); console.debug(message); console.debug(link); const captureURI = createCaptureURI(sender, message, link); console.debug(captureURI); crossPlatformOpen(captureURI); // TODO: Set fillColor to #1c97cc for (let path of event.target.getElementsByTagName('path')) { path.setAttribute('fill', '#1c97cc'); } }; // Find the "start_thread" button const laterButton = document.querySelector( 'button[data-qa="later"]' ); // Insert the new button before the "start_thread" button if (laterButton) { laterButton.parentNode.insertBefore(newButton, laterButton); console.debug("btn added"); } } function createCaptureURI(sender, message, link) { // new style const encoded_url = encodeURIComponent(link); const escaped_title = escapeIt(link); const escaped_message = escapeIt(sender + ": " + message); return "org-protocol://capture?url=" + encoded_url + "&title=" + escaped_title + "&body=" + escaped_message; } // From https://github.com/sprig/org-capture-extension/blob/3911377933619a24562730dc8b353424f8809d9e/capture.js#L87 function replace_all(str, find, replace) { return str.replace(new RegExp(find, 'g'), replace); } function escapeIt(text) { return replace_all( replace_all( replace_all( encodeURIComponent(text), "[(]", escape("(")), "[)]", escape(")")), "[']" ,escape("'")); } function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Function to observe mutations with debounce function observeMutations(targetNode) { // console.debug("observeMutations"); const debouncedInsertButton = debounce(insertButton, 50); // 200ms debounce // Create a mutation observer const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === "childList" || mutation.type === "attributes") { const messageActionsContainer = getMessageActionsContainer(); if ( messageActionsContainer && !isButtonPresent() ) { debouncedInsertButton(); } } } }); // Configure the observer to watch for changes in the subtree observer.observe(targetNode, { attributes: true, childList: true, subtree: true, }); observing = true; observeDisconnection(targetNode, observer); } function getMessageActionsContainer() { return document.querySelector( "div.c-message_actions__container.c-message__actions>div.c-message_actions__group" ); } function getCurrentMessageDetails() { const sender = document.querySelector( 'div.c-message_kit__hover--hovered span[data-qa$="-sender"]' ).innerText; const message = document.querySelector( 'div.c-message_kit__hover--hovered div.p-rich_text_section' ).innerText; const link = document.querySelector( 'div.c-message_kit__hover--hovered a.c-timestamp' ).href; return [sender, message, link]; } // Find the slack workspace element and add a hover event listener to start observing async function init() { const slackWorkspace = await getElement("div.p-client_workspace__layout"); if (slackWorkspace) { observeMutations(slackWorkspace); } else { console.error("couldnt find element"); } } init(); function isButtonPresent() { return document.querySelector("button.custom-org-capture-button"); } // helper menthod: get element whenever it becomes available function getElement(selector) { return new Promise((resolve, reject) => { // Check if the element already exists const element = document.querySelector(selector); if (element) { resolve(element); return; } // Create a MutationObserver to listen for changes in the DOM const observer = new MutationObserver((mutations, observer) => { // Check for the element again within each mutation const element = document.querySelector(selector); if (element) { observer.disconnect(); // Stop observing resolve(element); } }); // Start observing the document body for child list changes observer.observe(document.body, { childList: true, subtree: true }); // Set a timeout to reject the promise if the element isn't found within 30 seconds const timeoutId = setTimeout(() => { observer.disconnect(); // Ensure to disconnect the observer to prevent memory leaks resolve(null); // Resolve with null instead of rejecting to indicate the timeout without throwing an error }, 30000); // 30 seconds // Ensure that if the element is found and the observer is disconnected, we also clear the timeout observer.takeRecords().forEach((record) => { clearTimeout(timeoutId); }); }); } // Function to observe the targetNode getting deleted. // Have to observe the body node and check if targetNode still exists in the body function observeDisconnection(targetNode, targetObserver) { const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === "childList") { if (observing) { if (!document.body.contains(targetNode)) { observer.takeRecords(); observer.disconnect(); targetObserver.takeRecords(); targetObserver.disconnect(); observing = false; init(); } } } } }); observer.observe(document.body, { attributes: false, childList: true, subtree: true, }); }