Search code examples
javascripttampermonkeymutation-observers

MutationObserver script for Tampermonkey works in Firefox, but not in Chrome


Here's the script that saves the state of the elements as they appear (saveMessage function), and then uses MutationObserver to monitor the changes in the className of an element and to restore it's saved state (resetMessage function). Actually it was working in Firefox until one of the recent patches (v.104 I believe) and wasn't working in Chrome for at last a year (the text of the elements with className 'chat-line__message--deleted-notice' won't be restored, just nothing happens). Now it just does seemingly nothing in FF, but because it was working in FF and not working in Chrome at the same time, I assume it must be some very small deviation from the standard or something like that. Any ideas what's wrong with it?

// ==UserScript==
// @name         Twitch: <message deleted>
// @namespace    https://greasyfork.org/users/221926
// @version      1.3
// @description  show deleted messages in twitch chat
// @match      https://www.twitch.tv/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict'

  function saveMessage (el) {
    if (el.initialChildNodes) return
    el.initialChildNodes = Array.from(el.childNodes)
  }

  function resetMessage (el) {
    while (el.initialChildNodes == null) { el = el.parentNode }
    while (el.lastChild) { el.removeChild(el.lastChild) }
    el.initialChildNodes.forEach(childNode => el.appendChild(childNode))
    el = el.closest('.chat-line__message')
    el.style.backgroundColor = 'rgba(255, 0, 0, .1)'
    el.style.boxShadow = 'inset 4px 0 0 0 rgba(255, 0, 0, .4)'
  }

  new MutationObserver(mutationList => {
    mutationList.forEach(mutation => {
      Array.from(mutation.addedNodes).forEach(el => {
        switch (el.className) {
          case 'chat-line__message': saveMessage(el.querySelector('.chat-line__username-container').parentNode); break
          case 'chat-line__message--deleted-notice': resetMessage(el); break
        }
      })
    })
  }).observe(document.body, { childList: true, subtree: true, characterData: true })
})()

Solution

  • It looks like Twitch now replaces the whole .chat-line__message node, so you can't save the initial state in the node itself any longer. However, there is a solution: In this case replacing the element means that you get a mutation with a removal of a chat-line__message node followed by an added chat-line__message node.

    The following code saves that removed node, then, when the new deleted message node is added, restores the old node before the new deleted message node and deletes that new node.

    setting node.isrestored prevents a loop, because our code als triggers the MutationObserver.

    (function () {
      'use strict'
      var previousMutationRemoved;
    
      function resetMessage (el) {
        if (previousMutationRemoved == null) return;
        previousMutationRemoved.style.backgroundColor = 'rgba(255, 0, 0, .1)';
        previousMutationRemoved.style.boxShadow = 'inset 4px 0 0 0 rgba(255, 0, 0, .4)';
        previousMutationRemoved.isrestored = 'true';
        el.parentNode.insertBefore(previousMutationRemoved.cloneNode(true), el);
        previousMutationRemoved = null;
        el.isrestored = 'true';
        el.parentNode.removeChild(el);
      }
    
      new MutationObserver(mutationList => {
        mutationList.forEach(mutation => {
          Array.from(mutation.removedNodes).forEach(node => {
            if (node.nodeType == Node.ELEMENT_NODE) {
              if (node.className == 'chat-line__message' && node.isrestored != 'true') {
                previousMutationRemoved = node;
              }
            }
          });
          Array.from(mutation.addedNodes).forEach(node => {
            if (node.nodeType == Node.ELEMENT_NODE) {
              if (node.className == 'chat-line__message' && node.isrestored != 'true') {
                if(node.querySelector(".chat-line__message--deleted-notice")) {
                  resetMessage(node);
                }
              }
            }
          })
        });
        previousMutationRemoved = null;
      }).observe(document.body, { childList: true, subtree: true, characterData: true })
    })()