WhatsApp Web: Unblur with Scroll

Blur WhatsApp Web content (names, DPs, media, emojis) with last 3 messages unblurred. Robust on scroll/load. Hover unblurs. Stable & maintainable.

// ==UserScript==
// @name         WhatsApp Web: Unblur with Scroll
// @namespace    wwpb_robust_cleaned_minimal
// @version      1.0
// @description  Blur WhatsApp Web content (names, DPs, media, emojis) with last 3 messages unblurred. Robust on scroll/load. Hover unblurs. Stable & maintainable.
// @author       https://greasyfork.org/en/users/1356925-govind-rajulu & Perplexity AI
// @match        https://web.whatsapp.com/
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const UNCENSOR_LAST_N = 3;

  const CLS = {
    MSG_BLUR: 'wwpb-blur-msg',
    MEDIA_BLUR: 'wwpb-blur-media',
    DP_BLUR: 'wwpb-blur-dp',
    NAME_BLUR: 'wwpb-blur-name'
  };

  const SELECTORS = {
    SIDEBAR_AVATARS: [
      'img._ao3e',
      'img[class*="_ao3e"]',
      'img[class*="x1n2onr6"]',
      'img[class*="xh8yej3"]',
      'img._2WP9Q',
      'img[src*="blob"]',
      'img[decoding="async"]',
      'div.x10l6tqk.x1agz8ms.xiy17q3.x18d0r48.xso031l.x1q0q8m5.x1jpdw5n.x5yr21d.x10l6tqk.xh8yej3[style*="background-image"]'
    ].join(','),
    SIDEBAR_NAMES: [
      '#pane-side ._21S-L span[dir]',
      '#pane-side span[title]',
      '#pane-side ._21S-L span'
    ].join(','),

    CHAT_MESSAGES: 'div.message-in, div.message-out',
    CHAT_MSG_TEXT: 'span.selectable-text',
    CHAT_SPECIAL_SPANS: [
      'span._ahxu',
      'span[class*="_ahxu"]',
      'span._ao3e',
      'span[class*="_ao3e"]',
      'span[class*="x1iyjqo2"]'
    ].join(','),
    CHAT_EMOJIS: [
      'img[data-emoji]',
      'img.emoji',
      'img[alt*="emoji"]',
      'img[src*="emoji"]',
      'span._ahiu',
      'span[class*="_ahiu"]'
    ].join(','),
    CHAT_MEDIA: 'img, video',

    CHAT_HEADER_NAMES: [
      '#main header span[title]',
      '#main header div[role="heading"] span',
      '#main header span[dir="auto"][title]'
    ].join(','),

    CHAT_HEADER_SPECIAL_SPANS: [
      'span._ao3e',
      'span[class*="_ao3e"]',
      'span._ahxu',
      'span[class*="_ahxu"]',
      'span[class*="x1iyjqo2"]'
    ].join(','),
    CHAT_HEADER_AVATAR: '#main header img',
    CHAT_HEADER_DIV_AVATAR: '#main header div[style*="background-image"]',

    CHAT_SIDE_NAMES: 'span._21S-L, span._3FuDI, span[title]'
  };

  const STYLE_ID = 'wwpb-blur-style-robust-cleaned-minimal';
  if (!document.getElementById(STYLE_ID)) {
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
      .${CLS.MSG_BLUR},
      .${CLS.MEDIA_BLUR},
      .${CLS.DP_BLUR} {
        filter: blur(5px);
        transition: filter 0.16s;
        cursor: pointer;
        display: inline-block;
      }
      .${CLS.DP_BLUR} {
        filter: blur(10px);
      }
      .${CLS.MSG_BLUR}:hover,
      .${CLS.MEDIA_BLUR}:hover,
      .${CLS.DP_BLUR}:hover,
      .${CLS.MEDIA_BLUR}:hover img,
      .${CLS.MSG_BLUR}:hover img {
        filter: none !important;
      }
      .${CLS.NAME_BLUR} {
        color: transparent;
        text-shadow: 0 0 5px rgba(0,0,0,0.6);
        user-select: none;
        cursor: pointer;
        transition: color 0.12s, text-shadow 0.12s;
      }
      .${CLS.NAME_BLUR}:hover {
        color: inherit;
        text-shadow: none;
      }
    `;
    document.head.appendChild(style);
  }

  // toggleClass
  function toggleClass(elements, className, condition) {
    elements.forEach(el => {
      if (el && el.classList) {
        condition ? el.classList.add(className) : el.classList.remove(className);
      }
    });
  }

  // debounce
  function debounce(fn, delay) {
    let timeoutId;
    return function (...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn.apply(this, args), delay);
    };
  }

  // getActiveMessageList
  function getActiveMessageList() {
    return (
      document.querySelector('div[tabindex="-1"][role="region"]') ||
      document.querySelector('div[aria-label="Message list"]') ||
      document.querySelector('div[data-testid="chat-list"]') ||
      document.querySelector('#main')
    );
  }

  // blurContactsSidebar
  function blurContactsSidebar() {
    const pane = document.getElementById('pane-side');
    if (!pane) return;
    toggleClass(Array.from(pane.querySelectorAll(SELECTORS.SIDEBAR_NAMES)), CLS.NAME_BLUR, true);
    toggleClass(Array.from(pane.querySelectorAll(SELECTORS.SIDEBAR_AVATARS)), CLS.DP_BLUR, true);
  }

  // blurChatMessages
  function blurChatMessages() {
    const messages = Array.from(document.querySelectorAll(SELECTORS.CHAT_MESSAGES));
    const total = messages.length;

    messages.forEach((msg, idx) => {
      const shouldBlur = total > UNCENSOR_LAST_N && idx < total - UNCENSOR_LAST_N;

      Array.from(
        msg.querySelectorAll(
          [
            SELECTORS.CHAT_MSG_TEXT,
            SELECTORS.CHAT_SPECIAL_SPANS,
            SELECTORS.CHAT_EMOJIS,
            SELECTORS.CHAT_MEDIA
          ].join(',')
        )
      ).forEach(el => {
        el.classList.remove(CLS.MSG_BLUR);
        el.classList.remove(CLS.MEDIA_BLUR);
      });

      if (shouldBlur) {
        toggleClass(Array.from(msg.querySelectorAll(SELECTORS.CHAT_MSG_TEXT)), CLS.MSG_BLUR, true);
        toggleClass(Array.from(msg.querySelectorAll(SELECTORS.CHAT_SPECIAL_SPANS)), CLS.MSG_BLUR, true);
        Array.from(msg.querySelectorAll(SELECTORS.CHAT_EMOJIS)).forEach(el => el.classList.add(CLS.MEDIA_BLUR));
      }
    });
  }

  // blurChatMedia
  function blurChatMedia() {
    const messages = Array.from(document.querySelectorAll(SELECTORS.CHAT_MESSAGES));
    const total = messages.length;

    messages.forEach((msg, idx) => {
      const shouldBlur = total > UNCENSOR_LAST_N && idx < total - UNCENSOR_LAST_N;
      Array.from(msg.querySelectorAll(SELECTORS.CHAT_MEDIA)).forEach(el => {
        if (shouldBlur && !el.classList.contains('emoji') && !el.classList.contains(CLS.MEDIA_BLUR)) {
          el.classList.add(CLS.MEDIA_BLUR);
        } else if (!shouldBlur && !el.classList.contains('emoji')) {
          el.classList.remove(CLS.MEDIA_BLUR);
        }
      });
    });
  }

  // blurChatHeader
  function blurChatHeader() {
    const header = document.querySelector('#main header');
    if (!header) return;
    toggleClass(Array.from(header.querySelectorAll(SELECTORS.CHAT_HEADER_NAMES)), CLS.NAME_BLUR, true);
    toggleClass(Array.from(header.querySelectorAll(SELECTORS.CHAT_HEADER_SPECIAL_SPANS)), CLS.NAME_BLUR, true);

    const avatarImg = header.querySelector(SELECTORS.CHAT_HEADER_AVATAR);
    if (avatarImg && avatarImg.classList) avatarImg.classList.add(CLS.DP_BLUR);

    const avatarDiv = header.querySelector(SELECTORS.CHAT_HEADER_DIV_AVATAR);
    if (avatarDiv && avatarDiv.classList) avatarDiv.classList.add(CLS.DP_BLUR);
  }

  // blurChatSideNames
  function blurChatSideNames() {
    toggleClass(Array.from(document.querySelectorAll(SELECTORS.CHAT_SIDE_NAMES)), CLS.NAME_BLUR, true);
  }

  // blurAllChatElements
  function blurAllChatElements() {
    blurChatMessages();
    blurChatMedia();
    blurChatHeader();
    blurChatSideNames();
  }

  const debouncedBlurAllChatElements = debounce(blurAllChatElements, 150);

  // observeContactSidebarChanges
  function observeContactSidebarChanges() {
    const pane = document.getElementById('pane-side');
    if (!pane) return;
    blurContactsSidebar();
    const observer = new MutationObserver(debounce(blurContactsSidebar, 100));
    observer.observe(pane, { childList: true, subtree: true });
  }

  // observeChatChanges
  function observeChatChanges() {
    const msgList = getActiveMessageList();
    if (!msgList) return;

    blurAllChatElements();

    const observer = new MutationObserver(() => {
      debouncedBlurAllChatElements();
    });
    observer.observe(msgList, { childList: true, subtree: true });

    let scrollTimeout;
    msgList.addEventListener('scroll', () => {
      clearTimeout(scrollTimeout);
      scrollTimeout = setTimeout(() => {
        blurAllChatElements();
      }, 250);
    });

    msgList.addEventListener('mousemove', () => {
      setTimeout(() => blurAllChatElements(), 200);
    });
  }

  // delayedBlurAllChatElements
  function delayedBlurAllChatElements(delay = 800) {
    setTimeout(blurAllChatElements, delay);
  }

  // setupObserversAndEvents
  function setupObserversAndEvents() {
    observeContactSidebarChanges();
    observeChatChanges();

    ['focus', 'mouseup', 'hashchange'].forEach(evt => {
      window.addEventListener(evt, () => delayedBlurAllChatElements(800));
    });

    document.addEventListener('visibilitychange', () => {
      if (!document.hidden) setTimeout(blurAllChatElements, 200);
    });

    setInterval(() => {
      const anyUnblurred = !!document.querySelector(
        'div.message-in span.selectable-text:not(.' + CLS.MSG_BLUR + '), ' +
        'div.message-out span.selectable-text:not(.' + CLS.MSG_BLUR + ')'
      );
      if (anyUnblurred) blurAllChatElements();
    }, 2500);
  }

  // waitForElement
  function waitForElement(selector, root = document.body, timeout = 8000) {
    return new Promise((resolve, reject) => {
      const el = root.querySelector(selector);
      if (el) {
        resolve(el);
        return;
      }
      const observer = new MutationObserver((mutations, obs) => {
        const found = root.querySelector(selector);
        if (found) {
          obs.disconnect();
          resolve(found);
        }
      });
      observer.observe(root, { childList: true, subtree: true });
      if (timeout) {
        setTimeout(() => {
          observer.disconnect();
          console.warn(`Timeout waiting for selector "${selector}". Retrying...`);
          reject(new Error(`Timeout waiting for selector: ${selector}`));
        }, timeout);
      }
    });
  }

  // init
  async function init() {
    try {
      await waitForElement('#pane-side');
      await waitForElement('#main');
      setupObserversAndEvents();
    } catch (error) {
      console.error('wwpb: Initialization failed:', error);
      setTimeout(init, 1000);
    }
  }

  init();

})();