Search code examples
javascriptgoogle-chromegoogle-chrome-extension

Chrome extension for sending WhatsApp messages with name


I'm trying to create a chrome extension for sending messages. I just want to add my name above the messages.

The problem is that I don't know how to send my name that comes from the variable (nomeAtendente).

I would be very grateful for any help, as I've been searching for how to do this on the internet for a few days now and haven't found anything.

enter image description here

const interval = setInterval(() => {
    const menu = document.querySelector("#app > div > div > div._2Ts6i._3RGKj > header > div._604FD > div > span > span");
    const content = document.querySelector("#app > div > div > div._2Ts6i._2xAQV");

    if (menu && content) {
        clearInterval(interval);

        const btnMenu = document.createElement("button");
        btnMenu.innerHTML = "7C";
        btnMenu.classList.add("btnMenu7Carros");
        menu.appendChild(btnMenu);



        const divLateralHTML = `
            Nome do atendente: <br>
            <input type="text" id="nomeAtendente">

            <br><br>
            <button id="buttonSalvar" name="button" class="">Salvar</button>
        `;
        content.insertAdjacentHTML('afterend', `<div id="divLateral" style="display: none;">${divLateralHTML}</div>`);


        //Abre e fecha o menu.
        const divLateral = document.getElementById("divLateral");
        let divLateralVisivel = false;
        btnMenu.addEventListener("click", function () {
            divLateralVisivel = !divLateralVisivel;
            divLateral.style.display = divLateralVisivel ? "block" : "none";
        });


        //Salva os dados
        const buttonSalvar = document.getElementById("buttonSalvar");
        buttonSalvar.addEventListener("click", function () {
            localStorage.setItem("nomeAtendente", document.getElementById("nomeAtendente").value);
            alert("Salvo com sucesso!")
        });
        // Obtém o valor do localStorage e define no input
        document.getElementById("nomeAtendente").value = localStorage.getItem('nomeAtendente') || "";



        const nomeAtendente = localStorage.getItem("nomeAtendente");





        document.addEventListener("keydown", function (e) {
            handleEvent(e);
        });

        document.addEventListener("keyup", function (e) {
            handleEvent(e);
        });

        document.addEventListener("keypress", function (e) {
            handleEvent(e);
        });

        function handleEvent(e) {
            const elemento = document.querySelector("#main > footer > div._2lSWV._3cjY2.copyable-area > div > span:nth-child(2) > div > div._1VZX7 > div._3Uu1_ > div > div > p > span");

            if (elemento !== null) {
                mensagemOriginal = elemento.textContent.trim();
                localStorage.setItem("mensagemOriginal", mensagemOriginal);
                //console.log("Original: " + mensagemOriginal);
            }


            if (e.key === "Enter" || e.code === "Enter") {
                mensagemOriginal = localStorage.getItem('mensagemOriginal') || ""
                const novaMensagem = `*[${nomeAtendente}]*\n${mensagemOriginal}`;
                console.log(novaMensagem);


                var elementoRemover = document.querySelector("#main > footer > div._2lSWV._3cjY2.copyable-area > div > span:nth-child(2) > div > div._1VZX7 > div._3Uu1_ > div > div.lhggkp7q.qq0sjtgm.jxacihee.c3x5l3r8.b9fczbqn.t35qvd06.m62443ks.rkxvyd19.c5h0bzs2.bze30y65.kao4egtt.kh4n4d4z.tt14wmjx");
                if (elementoRemover) {
                    elementoRemover.parentNode.removeChild(elementoRemover);
                }

                // Selecione o elemento com a classe "selectable-text" e classe "copyable-text"
                var elementoModificar = document.querySelector("#main > footer > div._2lSWV._3cjY2.copyable-area > div > span:nth-child(2) > div > div._1VZX7 > div._3Uu1_ > div > div > p");
                if (elementoModificar) {
                    elementoModificar.innerHTML = elementoModificar.innerHTML.replace(/<br>/g, '');
                
                    elementoModificar.innerHTML += `<span class="selectable-text copyable-text" data-lexical-text="true">${novaMensagem}</span>`;
                }



                localStorage.setItem("mensagemOriginal", "");
            }
        }


    }
}, 1000);


Solution

  • My first thought was to hook the function WhatsApp Web used to submit the message, or change the internal state of the app. Unfortunately I was not able to actually pin-point them after fiddling with the page.

    What I tried after, which seems to work, is this:

    1. get the editor element
    2. add a 'keydown' event listener (with 'capture:true')
    3. handle Enter keypress, and modify the message
    const editorElement = document.querySelector('#main div[data-lexical-editor]');
    
    function modifyMessage() {
      const editorSpan = editorElement.querySelector('span[data-lexical-text]'); // get span[data-lexical-text]
      console.log('editorSpan:', editorSpan);
      if (editorSpan) { // might not exist if the input field is empty
        const origMessage = editorSpan.childNodes[0].data; // get original message from text node (first child)
        const newMessage = '*AUTHOR*\n' + origMessage;
        editorSpan.childNodes[0].data = newMessage; // alter it!
        console.log('origMessage:', `"${origMessage}"`, 'newMessage:', `"${editorSpan.childNodes[0].data}"`);
        console.log('origMessage:', `"${origMessage}"`, 'newMessage:', `"${editorSpan.childNodes[0].data}"`);
      }
    }
    
    function handleEvent(e) {
      console.log('handleEvent:', e);
      if (e.type === 'keydown') {
        if (e.key === 'Enter') {
          modifyMessage();
        }
      }
    }
    
    const USE_CAPTURE = true;
    editorElement.addEventListener('keydown', handleEvent, {capture:USE_CAPTURE}); // NOTE: 'capture:true' may be needed in this case, to ensure the key event is captured before WA handles it */
    // editorElement.removeEventListener('keydown', handleEvent, USE_CAPTURE);
    

    With a few adjustments the same logic could be used to handle 'onClick' on the send button.

    Hopefully this is helpful, or at least a good starting point. ;D

    enter image description here

    UPDATE #0

    And that ~would work for a single chat... but what if you selected another chat?!

    Enter MutationObserver...

    UPDATE #1

    The MutationObserver API let's you listen for changes to the DOM and react to them.

    With the following code we listen for a new chat element being added to the DOM (matching the '#main' selector), and wire the events to the new editor element.

    // ~SAME CODE AS BEFORE
    
    function modifyMessage() {
      const editorSpan = editorElement.querySelector('span[data-lexical-text]'); // get span[data-lexical-text]
      console.log('editorSpan:', editorSpan);
      if (editorSpan) { // might not exist if the input field is empty
        const origMessage = editorSpan.childNodes[0].data; // get original message from text node (first child)
        const newMessage = '*AUTHOR* 😎\n' + origMessage;
        editorSpan.childNodes[0].data = newMessage; // alter it!
        console.log('origMessage:', `"${origMessage}"`, 'newMessage:', `"${editorSpan.childNodes[0].data}"`);
      }
    }
    
    function handleEvent(e) {
      console.log('handleEvent:', e);
      if (e.type === 'keydown') {
        if (e.key === 'Enter') {
          modifyMessage();
        }
      }
    }
    
    // NEW CODE
    
    function waitForElement(selector, callback) {
      // check if element matching selector is already in the DOM
      let matchedElement = document.querySelector(selector);
      if (matchedElement) {
        callback(matchedElement);
      } 
      
      // setup a MutationObserver that listens to changes to the DOM
      let mutObserver = new MutationObserver((mutationRecords, observer) => {
        for (const mutation of mutationRecords) {
          for (const addedNode of mutation.addedNodes) {
            if (addedNode.matches(selector)) {
              callback(addedNode);
            }
          }
        }
      });
      const options = {
        childList: true,
        subtree: true,
      };
      mutObserver.observe(document.documentElement, options);
      
      return mutObserver; // so that you can call .disconnect() to stop observing
    }
    
    let editorElement = null; // this will store the editorElement for the current chat
    
    // watch for chat change
    let observer = waitForElement('#main', (matchedElement) => { 
      console.log('matched (new chat clicked):', matchedElement);
      let newEditorElement = document.querySelector('#main div[data-lexical-editor]');
      if (editorElement != newEditorElement) { // check if it's actually a different editorElement
        editorElement = newEditorElement;
        console.log('new editorElement:', editorElement);
    
        const USE_CAPTURE = true;
        editorElement.addEventListener('keydown', handleEvent, {capture:USE_CAPTURE}); // NOTE: 'capture:true' may be needed in this case, to ensure the key event is captured before WA handles it */
        // editorElement.removeEventListener('keydown', handleEvent, USE_CAPTURE);
      }
    });
    // observer.disconnect();
    

    UPDATE #2

    Slightly altered code.

    Now the approach is to listen for added nodes inside the right panes (matched by '._2xAQV' - check if that's the case on your end too), and rewire the events as needed.

    Also adds/removes a click event listener to the send buttons, and keydown events on the correct text-inputs (should work for attachments too now).

    // ~SAME CODE AS BEFORE
    
    function modifyMessage() {
      const editorSpan = editorElement.querySelector('span[data-lexical-text]'); // get span[data-lexical-text]
      console.log('editorSpan:', editorSpan);
      if (editorSpan) { // might not exist if the input field is empty
        const origMessage = editorSpan.childNodes[0].data; // get original message from text node (first child)
        const newMessage = '*AUTHOR* 😎\n' + origMessage;
        editorSpan.childNodes[0].data = newMessage; // alter it!
        console.log('origMessage:', `"${origMessage}"`, 'newMessage:', `"${editorSpan.childNodes[0].data}"`);
      }
    }
    
    function handleEvent(e) {
      console.log('handleEvent:', e);
      if ((e.type === 'keydown' && e.key === 'Enter') || e.type === 'click') {
        modifyMessage();
      }
    }
    
    // SLIGHTLY MODIFIED CODE (should also handle send button and text messages below images)
    
    function waitForElement(selector, callback) {
      // check if element matching selector is already in the DOM
      let matchedElement = document.querySelector(selector);
      if (matchedElement) {
        callback(matchedElement);
      } 
      
      // setup a MutationObserver that listens to changes to the DOM
      let mutObserver = new MutationObserver((mutationRecords, observer) => {
        for (const mutation of mutationRecords) {
          if (mutation.addedNodes?.length > 0) console.log('addedNodes:', mutation.addedNodes);
          for (const addedNode of mutation.addedNodes) {
            if (addedNode.matches(selector)) {
              callback(addedNode);
            }
          }
          /*if (mutation.removedNodes?.length > 0) console.log('removedNodes:', mutation.removedNodes);
          for (const removedNode of mutation.removedNodes) {
            if (removedNode.matches(selector)) {
              callback(removedNode);
            }
          }*/
        }
      });
      const options = {
        childList: true,
        subtree: true,
      };
      mutObserver.observe(document.documentElement, options);
      
      return mutObserver; // so that you can call .disconnect() to stop observing
    }
    
    let editorElement = null; // this will store the editorElement for the current chat
    let sendButtonElement = null; // this will store the sendButtonElement for the current chat
    
    // watch for changes to the DOM (for elements added inside "._2xAQV" (which match the right panes))
    let observer = waitForElement('._2xAQV *', (matchedElement) => {
      console.log('matchedElement:', matchedElement);
      
      // editor
      const editorSelector = '._2xAQV .lexical-rich-text-input'; //'#main div[data-lexical-editor]'
      let newEditorElement = document.querySelector(editorSelector);
      if (editorElement != newEditorElement) { // check if it's actually a different editorElement
        editorElement = newEditorElement;
        console.log('new editorElement:', editorElement);
    
        const USE_CAPTURE = true;
        if (editorElement) editorElement.removeEventListener('keydown', handleEvent, USE_CAPTURE); // remove event listener
        editorElement.addEventListener('keydown', handleEvent, {capture:USE_CAPTURE}); // NOTE: 'capture:true' may be needed in this case, to ensure the key event is captured before WA handles it */
      }
    
      // send button
      const sendButtonSelector = '._2xAQV span[data-icon="send"]'; //'#main div[data-lexical-editor]'
      let newSendButtonElement = document.querySelector(sendButtonSelector)?.parentElement;
      if (sendButtonElement != newSendButtonElement) { // check if it's actually a different sendButtonElement
        sendButtonElement = newSendButtonElement;
        console.log('new newSendButtonElement:', sendButtonElement);
    
        const USE_CAPTURE = true;
        if (sendButtonElement) sendButtonElement.removeEventListener('click', handleEvent, USE_CAPTURE); // remove event listener
        sendButtonElement.addEventListener('click', handleEvent, {capture:USE_CAPTURE}); // NOTE: 'capture:true' may be needed in this case, to ensure the click event is captured before WA handles it */
      }
    });
    // observer.disconnect();
    

    Could be refactored a bit, but you should get the gist of it. Anyway let me know if it works for you.