WhatsApp Web Mention Everyone

Automatically tag everyone in a group chat on WhatsApp Web

// ==UserScript==
// @name            WhatsApp Web Mention Everyone
// @namespace       AlejandroAkbal
// @version         0.1.2
// @description     Automatically tag everyone in a group chat on WhatsApp Web
// @author          Alejandro Akbal
// @license         AGPL-3.0
// @icon            https://www.google.com/s2/favicons?sz=64&domain=whatsapp.com
// @homepage        https://github.com/AlejandroAkbal/WhatsApp-Web-Mention-Everyone-Userscript
// @match           https://web.whatsapp.com/*
// @grant           none
// @run-at          document-idle
// ==/UserScript==

/** @param {number} ms
 * @returns {Promise<void>}
 */
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

/**
 * Wait for an element matching the given selector to appear in the DOM
 * @param {string} selector - The CSS selector to match
 * @param {Object} [options={}] - Additional options
 * @param {number} [options.timeout=10000] - The number of milliseconds to wait before timing out
 * @param {boolean} [options.subtree=true] - Whether to observe the entire subtree or just the target node
 * @param {boolean} [options.childList=true] - Whether to observe added and removed nodes
 * @returns {Promise<Element>} - A promise that resolves with the matched element
 */
async function waitForElement(selector, options = {}) {
  const { timeout = 10000, subtree = true, childList = true } = options

  return new Promise((resolve, reject) => {
    let element
    let timeoutId

    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.matches && node.matches(selector)) {
            element = node

            observer.disconnect()

            clearTimeout(timeoutId)

            resolve(node)
            return
          }
        }
      }
    })

    observer.observe(document.documentElement, { subtree, childList })

    timeoutId = setTimeout(() => {
      observer.disconnect()

      if (element) {
        resolve(element)
      } else {
        reject(new Error(`Element not found: ${selector}`))
      }
    }, timeout)
  })
}

;(async function () {
  'use strict'

  console.info('WhatsApp Web Mention Everyone loaded.')

  let buffer = ''

  document.addEventListener('keyup', async (event) => {
    buffer += event.key

    // Keep the last 2 characters
    buffer = buffer.slice(-2)

    if (buffer === '@@') {
      buffer = ''

      // TODO: Delete the last 2 written characters (the "@@")

      try {
        await tagEveryone()
      } catch (error) {
        alert(error.message)
      }
    }
  })

  async function tagEveryone() {
    const groupSubtitle = document.querySelector("[data-testid='chat-subtitle'] > span")

    if (!groupSubtitle) {
      throw new Error('No chat subtitle found. Please open a group chat.')
    }

    let groupUsers = groupSubtitle.innerText.split(', ')

    if (groupUsers.length === 1) {
      throw new Error('No users found in the group chat. Please wait a second and try again.')
    }

    // Remove unnecessary text
    groupUsers = groupUsers.filter((user) => user !== 'You')

    // Normalize user's names without accents or special characters
    groupUsers = groupUsers.map((user) => user.normalize('NFD').replace(/[\u0300-\u036f]/g, ''))

    const chatInput = document.querySelector("[data-testid='conversation-compose-box-input'] > p")

    if (!chatInput) {
      throw new Error('No chat input found. Please type a letter in the chat input.')
    }

    for (const user of groupUsers) {
      document.execCommand('insertText', false, `@${user}`)

      // await waitForElement("[data-testid='contact-mention-list-item']")
      await sleep(300)

      // Send "tab" key to autocomplete the user
      const keyboardEvent = new KeyboardEvent('keydown', {
        key: 'Tab',
        code: 'Tab',
        keyCode: 9,
        which: 9,
        bubbles: true,
        cancelable: true,
        view: window
      })

      chatInput.dispatchEvent(keyboardEvent)

      document.execCommand('insertText', false, ' ')
    }
  }
})()