Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chrome extensions CORB: How to react to updates in the shared DOM

Trying to build a chrome extension content script that adds an extra useful nav to a GitHub issue page. When interaction is done through the normal webpage (the end user click an reaction emoji) - my injected element gets lost.

The only way I have been able to get around it is to set an interval that keeps removing and injecting my counter element into the page.

There must be a more elegant way than this that allows a responsive reaction to DOM changes so I can then remove and re-inject the element (instead of banging on the door all the time)?

The extension I'm trying to optimize can be found here

https://github.com/NorfeldtAbtion/github-issue-reactions-chrome-extension

The important files currently looks like this

addReactionsNav.js

const URL =
  window.location.origin + window.location.pathname + window.location.search
const header = document.querySelector('#partial-discussion-sidebar')
header.style = `position: relative;height: 100%;`
let wrapper = getWrapper()

// // The isolated world made it difficult to detect DOM changes in the shared DOM
// // So this monkey-hack to make it refresh when ..
// setInterval(() => {
//   wrapper.remove()
//   wrapper = getWrapper()
//   addReactionNav()
// }, 1000)

// Select the node that will be observed for mutations
const targetNode = document.querySelector('body')

// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true }

// Create an observer instance linked to the callback function
const observer = new MutationObserver(() => addReactionNav())

// Start observing the target node for configured mutations
observer.observe(targetNode, config)

function getWrapper() {
  const header = document.querySelector('#partial-discussion-sidebar')
  const wrapper = header.appendChild(document.createElement('div'))
  wrapper.style = `
      position:sticky;
      position: -webkit-sticky;
      top:10px;`
  return wrapper
}

function addReactionNav() {
  const title = document.createElement('div')
  title.style = `font-weight: bold`
  title.appendChild(document.createTextNode('Reactions'))
  wrapper.appendChild(title)

  // Grabbing all reactions Reactions �� �� �� �� ❤️ �� �� ��
  const reactionsNodes = document.querySelectorAll(`
    [alias="+1"].mr-1,
    [alias="rocket"].mr-1,
    [alias="tada"].mr-1,
    [alias="heart"].mr-1,
    [alias="smile"].mr-1,
    [alias="thinking_face"].mr-1,
    [alias="-1"].mr-1,
    [alias="eyes"].mr-1
  `)

  const reactionsNodesParents = [
    ...new Set(
      Array.from(reactionsNodes).map(node => node.parentElement.parentElement)
    ),
  ]

  reactionsNodesParents.forEach(node => {
    const a = document.createElement('a')
    const linkText = document.createTextNode('\n' + node.innerText)
    a.appendChild(linkText)
    a.title = node.innerText

    let id = null
    while (id == null || node != null) {
      if (node.tagName === 'A' && node.name) {
        id = node.name
        break
      }

      if (node.id) {
        id = node.id
        break
      }

      node = node.parentNode
    }
    const postURL = URL + '#' + id
    a.href = postURL
    a.style = `display:block;`

    wrapper.appendChild(a)
  })
}

manifest.json

{
  "manifest_version": 2,
  "name": "Github Issue Reactions",
  "version": "1.0",
  "description": "List a link of reactions on a github issue page",
  "permissions": ["https://www.github.com/", "http://www.github.com/"],
  "content_scripts": [
    {
      "matches": ["*://*.github.com/*/issues/*"],
      "js": ["addReactionsNav.js"],
      "run_at": "document_end"
    }
  ]
}

Found this brief mention about "isolated worlds"

https://youtu.be/laLudeUmXHM?t=79

Update

I now believe that the "bug" is due to CORB - which is a security measure against Spectre.

Cross-Origin Read Blocking (CORB) blocked cross-origin response https://api.github.com/_private/browser/stats with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

Google explain more about it in their talk Lessons from Spectre and Meltdown, and how the whole web is getting safer (Google I/O '18)

The example mentioned at 34:00 seems to have been blocked by CORB since.

like image 905
Norfeldt Avatar asked Jan 19 '20 00:01

Norfeldt


People also ask

Can Chrome extensions communicate with each other?

In addition to sending messages between different components in your extension, you can use the messaging API to communicate with other extensions.

How do I read a DOM extension in Chrome?

Run the extension Look for the hello world extension icon in Chrome toolbar and click on it. You should be able to see a window popup containing current tab page h1 dom element value.

How do I communicate with chrome extensions?

Chrome has documentation on sending messages from webpages to chrome extensions. You add an “externally_connectable” object to your manifest. This will specify which webpages you want to allow to communicate with your extension (in this case, localhost, since I was developing on my machine).


1 Answers

As GitHub replaces the whole #partial-discussion-sidebar node when "the end-user clicks a reaction emoji" on the first post, you need to getWrapper() again before addReactionNav() in your mutation observer response, as is shown below.

Update: As the #partial-discussion-sidebar node is not rerendered in case of reactions updating on posts other than the first one, we also need to respond to the timeline items' update.

const URL = window.location.origin + window.location.pathname + window.location.search;
const header = document.querySelector('#partial-discussion-sidebar');
header.style = `position: relative;height: 100%;`;
let wrapper = getWrapper();
addReactionNav();    // Initial display.

// Select the node that will be observed for mutations.
const targetNode = document.querySelector('body');

// Options for the observer (which mutations to observe).
const config = {
  childList: true,
  subtree: true
};

// Create an observer instance linked to the callback function.
const observer = new MutationObserver(mutations => {
  if (!targetNode.contains(wrapper) || mutations.some(mutation => mutation.target.matches('.js-timeline-item'))) {
    wrapper.remove();
    wrapper = getWrapper();
    addReactionNav();
  }
});

// Start observing the target node for configured mutations.
observer.observe(targetNode, config);
like image 118
Vincent W. Avatar answered Oct 17 '22 02:10

Vincent W.