Feedly Faviconize List

A user script to show feed favicons in Feedly Title-Only View.

// ==UserScript==
// @name            Feedly Faviconize List
// @namespace       jmln.tw
// @version         0.2.7
// @description     A user script to show feed favicons in Feedly Title-Only View.
// @author          Jimmy Lin
// @license         MIT
// @homepage        https://github.com/jmlntw/feedly-faviconize-list
// @supportURL      https://github.com/jmlntw/feedly-faviconize-list/issues
// @match           https://*.feedly.com/*
// @compatible      firefox
// @compatible      chrome
// @compatible      opera
// @run-at          document-end
// @grant           none
// ==/UserScript==

function addStyle (css) {
  const style = document.createElement('style')
  style.textContent = css
  document.head.appendChild(style)
  return style
}

addStyle(`
  .gm-favicon {
    width: 16px;
    height: 16px;
    margin: 0 6px 0 0;
    padding: 0;
    border-radius: 3px;
    vertical-align: top;
  }
`)

function awaitSelector (selector, root) {
  return new Promise((resolve, reject) => {
    try {
      const rootElement = root ?
        typeof root === 'string' ? document.querySelector(root) : root :
        document

      const findAndResolveElements = () => {
        const allElements = document.querySelectorAll(selector)
        const newElements = []
        const resolvedAttr = 'data-awaitselector-resolved'

        if (allElements.length > 0) {
          Array.prototype.slice.call(allElements)
            .filter(element => typeof element[resolvedAttr] === 'undefined')
            .forEach(element => {
              element[resolvedAttr] = true
              newElements.push(element)
            })

          if (newElements.length > 0) {
            observer.disconnect()
            resolve(newElements)
          }
        }
      }

      const observer = new MutationObserver(mutations => {
        const addedNodes = mutations.reduce((found, mutation) => {
          return found || mutation.addedNodes && mutation.addedNodes.length > 0
        })

        if (addedNodes) {
          findAndResolveElements()
        }
      })

      observer.observe(rootElement, {
        childList: true,
        subtree: true
      })

      findAndResolveElements()
    } catch (exception) {
      reject(exception)
    }
  })
}

function waitAwaitSelector (selector, root, callback) {
  (function awaiter () {
    const continueWatching = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : true

    if (continueWatching) {
      awaitSelector(selector, root).then(callback).then(awaiter)
    }
  }())
}

function createFavicon (url) {
  const domain = url.replace(/^https?:\/\/([^/:]+).*/i, '$1')
  const favicon = document.createElement('img')

  favicon.src = `https://www.google.com/s2/favicons?domain=${domain}&alt=feed`
  favicon.classList.add('gm-favicon')

  return favicon
}

awaitSelector('#feedlyPageFX', '#root').then(pages => {
  waitAwaitSelector('a.EntryMetadataSource[href]', pages[0], sources => {
    sources
      .filter(source => source.querySelector('.gm-favicon') === null)
      .forEach(source => {
        source.insertAdjacentElement('afterbegin', createFavicon(source.href))
      })
  })
})