6

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.

Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
Norfeldt
  • 8,272
  • 23
  • 96
  • 152
  • Use `MutationObserver` API, see the documentation/examples. – wOxxOm Jan 19 '20 at 04:39
  • Believe I tried something like that but I recall it only listened on my isolated DOM.. – Norfeldt Jan 19 '20 at 05:36
  • There's no such thing as isolated *DOM*. There's only isolated JavaScript world/context and ShadowDOM. The former isn't a problem for MutationObserver, the latter would require some trickery. If you still have the code you've tried, add it to the question as it was probably incorrect. – wOxxOm Jan 19 '20 at 06:21
  • I have added a code attempt to trying to use the `MutationObserver` - it's probably me that are doing something wrong.. – Norfeldt Jan 19 '20 at 06:42
  • What's causing the dom to change/re-render clicking on the emoji? There shouldn't be a dom change simply by clicking on a button. When a users clicks an emoji you should pass a message to your background.js to do whatever. – Tom Shaw Jan 19 '20 at 06:59
  • Also 1) GitHub uses History API for internal navigation between its pages so your content script won't even run if the initial github page in this tab didn't match the content_scripts pattern. 2) Your callback ignores the `mutations` parameter, instead it runs unconditionally on all DOM changes, including the irrelevant ones. – wOxxOm Jan 19 '20 at 07:02
  • 3) judging by the "hack" part in your code, another problem is that you need to update the reference to the wrapper which is not related to "shared DOM" (no such thing because all DOM is shared), but to the fact that GitHub uses History API for internal navigation so it may replace elements on the page without reloading it entirely (content scripts run only when the page is loaded entirely from the server which happens on github only on the initial navigation or manual page refresh). You can simply check if the wrapper is present before using it e.g. like `document.contains(wrapper)` – wOxxOm Jan 19 '20 at 07:18
  • @wOxxOm I have updated my question with more details of the issue. – Norfeldt Jan 21 '20 at 07:22
  • CORB is not related to your code because you're not making any network requests. – wOxxOm Jan 21 '20 at 07:37
  • Then why does the chrome dev console keep posting this warning every time I test my script? – Norfeldt Jan 21 '20 at 08:00

2 Answers2

1

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);
Vincent W.
  • 125
  • 6
  • Thank you, however you don't seem to use the config you created. The reaction div stays there but it does not update the values (adding a to one of the post) - you can try it out on https://github.com/microsoft/vscode/issues/5402 – Norfeldt Jan 28 '20 at 09:29
  • Well, it works fine when the reactions are done to the first post but not to others, as the `#partial-discussion-sidebar` node is not rerendered in those cases. So, this time just make some small changes and it will work for all the posts. (I've updated my answer.) – Vincent W. Jan 28 '20 at 21:24
  • Thank you! It works perfect. It was me who was setting up the `MutationObserver` incorrectly. Thank you for helping me out. – Norfeldt Jan 30 '20 at 07:39
0

If the current tab has been updated you can pass a message back to you content script to re inject your content.

Background.js

chrome.tabs.onUpdated.addListener(tabId, changeInfo, tab) => {
  if (changeInfo.url) {
    chrome.tabs.sendMessage(tabId, {
      message: actions.TAB_UPDATED
    });
  }
})

Content.js

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.message === actions.TAB_UPDATED) {
    // show buttons
  }
});
Norfeldt
  • 8,272
  • 23
  • 96
  • 152
Tom Shaw
  • 1,642
  • 2
  • 16
  • 25