Greasy Fork is available in English.

Slack: Org Protocol Capture

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,
  });
}