code:
// Modified from https://github.com/jackmoore/autosize/blob/113f1b3/src/autosize.js
// The only change is to remove IE-specific hacks,
// remove parent overflow checks, make page resizes more performant,
// add deferredUpdate, and add perf marks.

import { mark, stop } from '../../_utils/marks'
import debounce from 'lodash-es/debounce'
import throttle from 'lodash-es/throttle'
import { getScrollContainer } from '../../_utils/scrollContainer'

const map = new Map()
let createEvent = (name) => new Event(name, { bubbles: true })

function assign (ta) {
  if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || map.has(ta)) {
    return
  }

  // TODO: hack - grab our scroll container so we can maintain the scrollTop
  let container = getScrollContainer()
  let heightOffset = null
  let cachedHeight = null

  function init () {
    const style = window.getComputedStyle(ta, null)

    if (style.boxSizing === 'content-box') {
      heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom))
    } else {
      heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth)
    }

    update()
  }

  function resize () {
    mark('autosize:resize()')
    let res = _resize()
    stop('autosize:resize()')
    return res
  }

  function _resize () {
    const originalHeight = ta.style.height
    const scrollTop = container.scrollTop

    ta.style.height = '' // this may change the scrollTop in Firefox

    const endHeight = ta.scrollHeight + heightOffset

    if (ta.scrollHeight === 0) {
      // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
      ta.style.height = originalHeight
      return
    }

    ta.style.height = endHeight + 'px'
    container.scrollTop = scrollTop // Firefox jiggles if we don't reset the scrollTop of the container
    return endHeight
  }

  const deferredUpdate = throttle(() => requestAnimationFrame(update), 100)

  function update () {
    mark('autosize:update()')
    _update()
    stop('autosize:update()')
  }

  function _update () {
    let newHeight = resize()
    if (cachedHeight !== newHeight) {
      cachedHeight = newHeight
      const evt = createEvent('autosize:resized')
      try {
        ta.dispatchEvent(evt)
      } catch (err) {
        // Firefox will throw an error on dispatchEvent for a detached element
        // https://bugzilla.mozilla.org/show_bug.cgi?id=889376
      }
    }
  }

  const pageResize = debounce(() => requestAnimationFrame(update), 1000)

  const destroy = () => {
    window.removeEventListener('resize', pageResize, false)
    ta.removeEventListener('input', deferredUpdate, false)
    ta.removeEventListener('autosize:destroy', destroy, false)
    ta.removeEventListener('autosize:update', update, false)

    map.delete(ta)
  }

  ta.addEventListener('autosize:destroy', destroy, false)

  window.addEventListener('resize', pageResize, false)
  ta.addEventListener('input', deferredUpdate, false)
  ta.addEventListener('autosize:update', update, false)

  map.set(ta, {
    destroy,
    update
  })

  init()
}

function destroy (ta) {
  const methods = map.get(ta)
  if (methods) {
    methods.destroy()
  }
}

function update (ta) {
  const methods = map.get(ta)
  if (methods) {
    methods.update()
  }
}

let autosize = (el, options) => {
  if (el) {
    Array.prototype.forEach.call(el.length ? el : [el], x => assign(x, options))
  }
  return el
}
autosize.destroy = el => {
  if (el) {
    Array.prototype.forEach.call(el.length ? el : [el], destroy)
  }
  return el
}
autosize.update = el => {
  if (el) {
    Array.prototype.forEach.call(el.length ? el : [el], update)
  }
  return el
}

export { autosize }


code:
Copyright 2016, Jake Archibald

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

repo_name: RayoGundead/pinny

license: mit


code:
// Forked from https://github.com/jakearchibald/idb-keyval/commit/ea7d507
// Adds a function for closing the database, ala https://github.com/jakearchibald/idb-keyval/pull/65
class Store {
  constructor (dbName = 'keyval-store', storeName = 'keyval') {
    this.storeName = storeName
    this._dbName = dbName
    this._storeName = storeName
    this._init()
  }

  _withIDBStore (type, callback) {
    this._init()
    return this._dbp.then(db => new Promise((resolve, reject) => {
      const transaction = db.transaction(this.storeName, type)
      transaction.oncomplete = () => resolve()
      transaction.onabort = transaction.onerror = () => reject(transaction.error)
      callback(transaction.objectStore(this.storeName))
    }))
  }

  _init () {
    if (this._dbp) {
      return
    }
    this._dbp = new Promise((resolve, reject) => {
      const openreq = indexedDB.open(this._dbName, 1)
      openreq.onerror = () => reject(openreq.error)
      openreq.onsuccess = () => resolve(openreq.result)
      // First time setup: create an empty object store
      openreq.onupgradeneeded = () => {
        openreq.result.createObjectStore(this._storeName)
      }
    })
  }

  _close () {
    this._init()
    return this._dbp.then(db => {
      db.close()
      this._dbp = undefined
    })
  }
}

let store

function getDefaultStore () {
  if (!store) {
    store = new Store()
  }
  return store
}

function get (key, store = getDefaultStore()) {
  let req
  return store._withIDBStore('readonly', store => {
    req = store.get(key)
  }).then(() => req.result)
}

function set (key, value, store = getDefaultStore()) {
  return store._withIDBStore('readwrite', store => {
    store.put(value, key)
  })
}

function del (key, store = getDefaultStore()) {
  return store._withIDBStore('readwrite', store => {
    store.delete(key)
  })
}

function clear (store = getDefaultStore()) {
  return store._withIDBStore('readwrite', store => {
    store.clear()
  })
}

function keys (store = getDefaultStore()) {
  const keys = []
  return store._withIDBStore('readonly', store => {
    // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
    // And openKeyCursor isn't supported by Safari.
    (store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {
      if (!this.result) {
        return
      }
      keys.push(this.result.key)
      this.result.continue()
    }
  }).then(() => keys)
}

function close (store = getDefaultStore()) {
  return store._close()
}

export { Store, get, set, del, clear, keys, close }


code:
// adapted from https://unpkg.com/timeago.js@4.0.0-beta.1/lib/index.js
/**
 * Created by hustcc on 18/5/20.
 * Contract: i@hust.cc
 */

var IndexMapEn = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year']
var SEC_ARRAY = [60, 60, 24, 7, 365 / 7 / 12, 12]

/**
 * Created by hustcc on 18/5/20.
 * Contract: i@hust.cc
 */

function en (number, index) {
  if (index === 0) {
    return ['just now', 'right now']
  }
  var unit = IndexMapEn[Math.floor(index / 2)]
  if (number > 1) {
    unit += 's'
  }
  return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit]
}

/**
 * Created by hustcc on 18/5/20.
 * Contract: i@hust.cc
 */

/**
 * format the diff second to *** time ago, with setting locale
 * @param diff
 * @param locale
 * @param defaultLocale
 * @returns {string | void | *}
 */
function formatDiff (diff) {
  // if locale is not exist, use defaultLocale.
  // if defaultLocale is not exist, use build-in `en`.
  // be sure of no error when locale is not exist.
  var i = 0

  var agoin = diff < 0 ? 1 : 0

  // timein or timeago

  var totalSec = diff = Math.abs(diff)

  for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY.length; i++) {
    diff /= SEC_ARRAY[i]
  }
  diff = Math.floor(diff)
  i *= 2

  if (diff > (i === 0 ? 9 : 1)) i += 1
  return en(diff, i, totalSec)[agoin].replace('%s', diff)
}

/**
 * calculate the diff second between date to be formatted an now date.
 * @param date
 * @param nowDate
 * @returns {number}
 */
function diffSec (date, now) {
  return (now - date) / 1000
}

/**
 * Created by hustcc on 18/5/20.
 * Contract: i@hust.cc
 */
export function format (date, now) {
  return formatDiff(diffSec(date, now))
}


code:
// via https://github.com/jonschlinkert/unescape/blob/98d1e52/index.js

const chars = {
  '"': '"',
  '"': '"',

  ''': '\'',
  ''': '\'',

  '&': '&',
  '&': '&',

  '>': '>',
  '>': '>',

  '<': '<',
  '<': '<',

  '¢': '¢',
  '¢': '¢',

  '©': '©',
  '©': '©',

  '€': '€',
  '€': '€',

  '£': '£',
  '£': '£',

  '®': '®',
  '®': '®',

  '¥': '¥',
  '¥': '¥',

  ' ': ' '
}

let regex

/**
 * Convert HTML entities to HTML characters.
 *
 * @param  {String} `str` String with HTML entities to un-escape.
 * @return {String}
 */

function unescape (str) {
  regex = regex || toRegex(chars)
  return str.replace(regex, m => chars[m])
}

function toRegex (chars) {
  var keys = Object.keys(chars).join('|')
  return new RegExp('(' + keys + ')', 'g')
}

/**
 * Expose `unescape`
 */

export { unescape }


code:
import { slide as svelteSlide } from 'svelte-transitions'
import { store } from '../_store/store'
import noop from 'lodash-es/noop'

// same as svelte-transitions, but respecting reduceMotion
export function slide (node, ref) {
  let { reduceMotion } = store.get()
  if (reduceMotion) {
    return {
      delay: 0,
      duration: 1, // setting to 0 causes some kind of built-in duration
      easing: _ => _,
      css: noop
    }
  }
  return svelteSlide(node, ref)
}


code:
// Utility for throttling in the Lodash style (assuming leading: true and trailing: true) but
// creates a promise.
export class PromiseThrottler {
  constructor (timeout) {
    this._timeout = timeout
    this._promise = Promise.resolve()
  }

  next () {
    let res = this._promise
    // update afterwards, so we get a "leading" XHR
    this._promise = this._promise.then(() => new Promise(resolve => setTimeout(resolve, this._timeout)))
    return res
  }
}


code:
// A store where you can divide data into "realms" that are backed with an LRU cache.
// Each realm has self-contained data that you can set with setForRealm() and compute
// with computeForRealm(). The maxSize determines how many realms to keep in the LRU cache.
import { Store } from 'svelte/store.js'
import QuickLRU from 'quick-lru'
import { mark, stop } from './marks'

export class RealmStore extends Store {
  constructor (init, maxSize) {
    super(init)
    this.set({ realms: new QuickLRU({ maxSize: maxSize }) })
    this._batches = {}
  }

  setCurrentRealm (realm) {
    this.set({ currentRealm: realm })
  }

  setForRealm (obj) {
    let { currentRealm, realms } = this.get()
    realms.set(currentRealm, Object.assign(realms.get(currentRealm) || {}, obj))
    this.set({ realms: realms })
  }

  computeForRealm (key, defaultValue) {
    this.compute(key,
      ['realms', 'currentRealm'],
      (realms, currentRealm) => {
        let realmData = realms.get(currentRealm)
        return (realmData && realmData[key]) || defaultValue
      })
  }

  /*
   * Update several values at once in a realm, assuming the key points
   * to a plain old javascript object.
   */
  batchUpdateForRealm (key, subKey, value) {
    let { currentRealm } = this.get()
    let realmBatches = this._batches[currentRealm]
    if (!realmBatches) {
      realmBatches = this._batches[currentRealm] = {}
    }
    let batch = realmBatches[key]
    if (!batch) {
      batch = realmBatches[key] = {}
    }
    batch[subKey] = value

    requestAnimationFrame(() => {
      let batch = this._batches[currentRealm] && this._batches[currentRealm][key]
      if (!batch) {
        return
      }
      let updatedKeys = Object.keys(batch)
      if (!updatedKeys.length) {
        return
      }
      mark('batchUpdate')
      let obj = this.get()[key]
      for (let otherKey of updatedKeys) {
        obj[otherKey] = batch[otherKey]
      }
      delete this._batches[currentRealm][key]
      let { realms } = this.get()
      realms.set(currentRealm, Object.assign(realms.get(currentRealm) || {}, { [key]: obj }))
      this.set({ realms: realms })
      stop('batchUpdate')
    })
  }
}


code:
export const DEFAULT_TIMEOUT = 20000
export const MEDIA_WRITE_TIMEOUT = 90000 // media uploads can take awhile
export const WRITE_TIMEOUT = 45000 // allow more time if the user did a write action

function fetchWithTimeout (url, fetchOptions, timeout) {
  return new Promise((resolve, reject) => {
    fetch(url, fetchOptions).then(resolve, reject)
    setTimeout(() => reject(new Error(`Timed out after ${timeout / 1000} seconds`)), timeout)
  })
}

function makeFetchOptions (method, headers, options) {
  let res = {
    method,
    headers: Object.assign(headers || {}, {
      'Accept': 'application/json'
    })
  }
  if (options && options.signal) {
    res.signal = options.signal
  }
  return res
}

async function throwErrorIfInvalidResponse (response) {
  let json = await response.json()
  if (response.status >= 200 && response.status < 300) {
    return json
  }
  if (json && json.error) {
    throw new Error(response.status + ': ' + json.error)
  }
  throw new Error('Request failed: ' + response.status)
}

async function _fetch (url, fetchOptions, options) {
  let response
  if (options && options.timeout) {
    response = await fetchWithTimeout(url, fetchOptions, options.timeout)
  } else {
    response = await fetch(url, fetchOptions)
  }
  return throwErrorIfInvalidResponse(response)
}

async function _putOrPostOrPatch (method, url, body, headers, options) {
  let fetchOptions = makeFetchOptions(method, headers, options)
  if (body) {
    if (body instanceof FormData) {
      fetchOptions.body = body
    } else {
      fetchOptions.body = JSON.stringify(body)
      fetchOptions.headers['Content-Type'] = 'application/json'
    }
  }
  return _fetch(url, fetchOptions, options)
}

export async function put (url, body, headers, options) {
  return _putOrPostOrPatch('PUT', url, body, headers, options)
}

export async function post (url, body, headers, options) {
  return _putOrPostOrPatch('POST', url, body, headers, options)
}

export async function patch (url, body, headers, options) {
  return _putOrPostOrPatch('PATCH', url, body, headers, options)
}

export async function get (url, headers, options) {
  return _fetch(url, makeFetchOptions('GET', headers, options), options)
}

export async function del (url, headers, options) {
  return _fetch(url, makeFetchOptions('DELETE', headers, options), options)
}

export function paramsString (paramsObject) {
  let res = ''
  let count = -1
  Object.keys(paramsObject).forEach(key => {
    let value = paramsObject[key]
    if (Array.isArray(value)) { // rails convention for encoding multiple values
      for (let item of value) {
        res += (++count > 0 ? '&' : '') + encodeURIComponent(key) + '[]=' + encodeURIComponent(item)
      }
    } else {
      res += (++count > 0 ? '&' : '') + encodeURIComponent(key) + '=' + encodeURIComponent(value)
    }
  })
  return res
}


code:
// Lightweight polyfill for web animations API element.animate()
// This is good enough for my use case, although not a full polyfill
// of the entire API

// via https://stackoverflow.com/a/15710692
function hashCode (s) {
  return s.split('')
    .reduce((a, b) => {
      a = ((a << 5) - a) + b.charCodeAt(0)
      return a & a
    }, 0)
}

function generateCss (id, animations) {
  let keyframes = animations.map(({ properties }, i) => (
    `@keyframes keyframe-${id}-${i} {
      ${properties.map((obj, i) => (`${Math.round(100 * i / (properties.length - 1))}% {
          ${Object.keys(obj).map(key => `${key}: ${obj[key]};`).join('')}
        }`
    )).join('')}
    }`
  ))

  let animationCss = `.${id} {
    animation: ${animations.map(({ options }, i) => {
    return `keyframe-${id}-${i} ${options.duration}ms ${options.easing}`
  }).join(',')};
  }`

  return keyframes + animationCss
}

export function animate (el, animations) {
  if (typeof el.animate === 'function') {
    return animations
      .map(({ properties, options }) => el.animate(properties, options))
      .map(anim => anim.play())
  }

  let hash = hashCode(JSON.stringify(animations))
  let id = `anim-${hash}`

  if (!document.getElementById(id)) {
    let style = document.createElement('style')
    style.id = id
    style.textContent = generateCss(id, animations)
    document.head.appendChild(style)
  }

  requestAnimationFrame(() => {
    el.classList.add(id)
    let wait = Math.max.apply(Math, animations.map(({ options }) => options.duration))
    setTimeout(() => {
      requestAnimationFrame(() => {
        el.classList.remove(id)
      })
    }, wait)
  })
}


code:
// Merge two arrays, using the given comparator
export function mergeArrays (leftArray, rightArray, comparator) {
  let leftIndex = 0
  let rightIndex = 0
  let merged = []
  while (leftIndex < leftArray.length || rightIndex < rightArray.length) {
    if (leftIndex === leftArray.length) {
      merged.push(rightArray[rightIndex])
      rightIndex++
      continue
    }
    if (rightIndex === rightArray.length) {
      merged.push(leftArray[leftIndex])
      leftIndex++
      continue
    }
    let left = leftArray[leftIndex]
    let right = rightArray[rightIndex]
    let comparison = comparator(right, left)
    if (comparison === 0) {
      merged.push(left)
      rightIndex++
      leftIndex++
    } else if (comparison > 0) {
      merged.push(right)
      rightIndex++
    } else {
      merged.push(left)
      leftIndex++
    }
  }
  return merged
}

export function concat () {
  let res = []
  for (let i = 0, len = arguments.length; i < len; i++) {
    let arg = arguments[i]
    if (Array.isArray(arg)) {
      res = res.concat(arguments[i])
    } else {
      res.push(arguments[i])
    }
  }
  return res
}


code:
const getDefault = mod => mod.default

export const importTimeline = () => import(
  /* webpackChunkName: 'Timeline' */ '../_components/timeline/Timeline.html'
  ).then(getDefault)

export const importVirtualList = () => import(
  /* webpackChunkName: 'VirtualList.html' */ '../_components/virtualList/VirtualList.html'
  ).then(getDefault)

export const importList = () => import(
  /* webpackChunkName: 'List.html' */ '../_components/list/List.html'
  ).then(getDefault)

export const importStatusVirtualListItem = () => import(
  /* webpackChunkName: 'StatusVirtualListItem.html' */ '../_components/timeline/StatusVirtualListItem.html'
  ).then(getDefault)

export const importNotificationVirtualListItem = () => import(
  /* webpackChunkName: 'NotificationVirtualListItem.html' */ '../_components/timeline/NotificationVirtualListItem.html'
  ).then(getDefault)

export const importDatabase = () => import(
  /* webpackChunkName: 'database.js' */ '../_database/databaseApis.js'
  )

export const importLoggedInObservers = () => import(
  /* webpackChunkName: 'loggedInObservers.js' */ '../_store/observers/loggedInObservers.js'
  ).then(getDefault)

export const importNavShortcuts = () => import(
    /* webpackChunkName: 'NavShortcuts' */ '../_components/NavShortcuts.html'
  ).then(getDefault)

export const importEmojiMart = () => import(
  /* webpackChunkName: 'createEmojiMartPickerFromData.js' */ '../_react/createEmojiMartPickerFromData.js'
  ).then(getDefault)

export const importToast = () => import(
  /* webpackChunkName: 'Toast.html' */ '../_components/toast/Toast.html'
  ).then(getDefault)

export const importSnackbar = () => import(
  /* webpackChunkName: 'Snackbar.html' */ '../_components/snackbar/Snackbar.html'
  ).then(getDefault)


code:
export const importIntersectionObserver = () => import(
  /* webpackChunkName: '$polyfill$-intersection-observer' */ 'intersection-observer'
  )

export const importRequestIdleCallback = () => import(
  /* webpackChunkName: '$polyfill$-requestidlecallback' */ 'requestidlecallback'
  )

export const importIndexedDBGetAllShim = () => import(
  /* webpackChunkName: '$polyfill$-indexeddb-getall-shim' */ 'indexeddb-getall-shim'
  )

export const importCustomElementsPolyfill = () => import(
  /* webpackChunkName: '$polyfill$-@webcomponents/custom-elements' */ '@webcomponents/custom-elements'
  )


code:
const decodeBase64 = base64 => {
  const rawData = window.atob(base64)
  const outputArray = new Uint8Array(rawData.length)

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i)
  }

  return outputArray
}

// Taken from https://www.npmjs.com/package/web-push
export const urlBase64ToUint8Array = (base64String) => {
  const padding = '='.repeat((4 - base64String.length % 4) % 4)
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/')

  return decodeBase64(base64)
}


code:
// Check if some condition applies for a node or any of its ancestors,
// stopping at an element that returns true for the given stopFunc. Returns
// false if none match
export function checkDomAncestors (node, checkFunc, stopFunc) {
  let thisNode = node
  while (thisNode) {
    if (stopFunc(thisNode)) {
      break
    }
    if (checkFunc(thisNode)) {
      return true
    }
    thisNode = thisNode.parentElement
  }
  return false
}


code:
export function classname () {
  let res = ''
  let len = arguments.length
  let i = -1
  while (++i < len) {
    let item = arguments[i]
    if (item) {
      if (res) {
        res += ' '
      }
      res += item
    }
  }
  return res
}


code: export const coordsToPercent = coord => (1 + coord) / 2 * 100


code:
import { removeEmoji } from './removeEmoji'

export function createAutosuggestAccessibleLabel (
  autosuggestType, $omitEmojiInDisplayNames,
  selectedIndex, searchResults) {
  let selected = searchResults[selectedIndex]
  let label
  if (autosuggestType === 'emoji') {
    label = `${selected.shortcode}`
  } else { // account
    let displayName = selected.display_name || selected.username
    let emojis = selected.emojis || []
    displayName = $omitEmojiInDisplayNames
      ? removeEmoji(displayName, emojis) || displayName
      : displayName
    label = `${displayName} @${selected.acct}`
  }
  return `${label} (${selectedIndex + 1} of ${searchResults.length}). ` +
    `Press up and down arrows to review and enter to select.`
}


code:
export function createStatusOrNotificationUuid (currentInstance, timelineType, timelineValue, notificationId, statusId) {
  return `${currentInstance}/${timelineType}/${timelineValue}/${notificationId || ''}/${statusId || ''}`
}


code:
export function decodeImage (img) {
  if (typeof img.decode === 'function') {
    return img.decode()
  }

  return new Promise((resolve, reject) => {
    img.onload = resolve
    img.onerror = reject
  })
}


code:
// Per the web share target spec: https://wicg.github.io/web-share-target/
// U+0020 (SPACE) characters are encoded as "+", due to the use of
// application/x-www-form-urlencoded encoding, not "%20" as might be expected.

export function decodeURIComponentWithPluses (text) {
  return text.split('+').map(decodeURIComponent).join(' ')
}


code:
// Delegate certain events to the global document for perf purposes.

const callbacks = {}

if (process.browser && process.env.NODE_ENV !== 'production') {
  window.delegateCallbacks = callbacks
}

function onEvent (e) {
  let { type, keyCode, target } = e
  if (!(type === 'click' || (type === 'keydown' && keyCode === 13))) {
    // we're not interested in any non-click or non-Enter events
    return
  }
  let key
  let element = target
  while (element) {
    if ((key = element.getAttribute('id'))) {
      break
    }
    element = element.parentElement
  }
  if (key && callbacks[key]) {
    if (type === 'click') {
      let selection = window.getSelection()
      let selectionStr = selection && selection.toString()
      if (selectionStr && selectionStr.length && target.contains(selection.anchorNode)) {
        return // ignore if the user is selecting text inside the clickable area
      }
    }
    callbacks[key](e)
  }
}

export function registerClickDelegates (component, delegates) {
  Object.assign(callbacks, delegates)

  component.on('destroy', () => {
    Object.keys(delegates).forEach(key => {
      delete callbacks[key]
    })
  })
}

export function registerClickDelegate (component, key, callback) {
  registerClickDelegates(component, { [key]: callback })
}

if (process.browser) {
  document.addEventListener('click', onEvent)
  document.addEventListener('keydown', onEvent)
}


code:
export function doubleRAF (fn) {
  requestAnimationFrame(() => requestAnimationFrame(fn))
}


code:
import emojiRegex from 'emoji-regex/es2015/text'
import { thunk } from './thunk'

export const getEmojiRegex = thunk(emojiRegex)


code:
import { replaceAll } from './strings'
import { replaceEmoji } from './replaceEmoji'

export function emojifyText (text, emojis, autoplayGifs) {
  // replace native emoji with wrapped spans so we can give them the proper font-family
  text = replaceEmoji(text, substring => `<span class="inline-emoji">${substring}</span>`)

  // replace custom emoji
  if (emojis) {
    for (let emoji of emojis) {
      let urlToUse = autoplayGifs ? emoji.url : emoji.static_url
      let shortcodeWithColons = `:${emoji.shortcode}:`
      text = replaceAll(
        text,
        shortcodeWithColons,
        `<img class="inline-custom-emoji" draggable="false" src="${urlToUse}"
                    alt="${shortcodeWithColons}" title="${shortcodeWithColons}" />`
      )
    }
  }

  return text
}


code:
import EventEmitter from 'events-light'

const eventBus = new EventEmitter()

if (process.browser && process.env.NODE_ENV !== 'production') {
  window.eventBus = eventBus
}

export function on (eventName, component, method) {
  let callback = method.bind(component)
  eventBus.on(eventName, callback)
  component.on('destroy', () => {
    eventBus.removeListener(eventName, callback)
  })
}

export const emit = eventBus.emit.bind(eventBus)


code:
export function mouseover (node, callback) {
  function onMouseEnter () {
    callback(true) // eslint-disable-line
  }
  function onMouseLeave () {
    callback(false) // eslint-disable-line
  }
  node.addEventListener('mouseenter', onMouseEnter)
  node.addEventListener('mouseleave', onMouseLeave)
  return {
    destroy () {
      node.removeEventListener('mouseenter', onMouseEnter)
      node.removeEventListener('mouseleave', onMouseLeave)
    }
  }
}

export function focusWithCapture (node, callback) {
  node.addEventListener('focus', callback, true)
  return {
    destroy () {
      node.removeEventListener('focus', callback, true)
    }
  }
}

export function blurWithCapture (node, callback) {
  node.addEventListener('blur', callback, true)
  return {
    destroy () {
      node.removeEventListener('blur', callback, true)
    }
  }
}

export function selectionChange (node, callback) {
  let events = ['keyup', 'click', 'focus', 'blur']
  let listener = () => {
    callback(node.selectionStart)
  }
  for (let event of events) {
    node.addEventListener(event, listener)
  }
  return {
    destroy () {
      for (let event of events) {
        node.removeEventListener(event, listener)
      }
    }
  }
}

// in some cases we apply focus styles to parent rather
// than the node itself because it shows up better
export function applyFocusStylesToParent (node) {
  function onFocus () {
    node.parentElement.classList.toggle('focus', true)
  }

  function onBlur () {
    node.parentElement.classList.toggle('focus', false)
  }

  node.addEventListener('focus', onFocus)
  node.addEventListener('blur', onBlur)

  return {
    destroy () {
      node.removeEventListener('focus', onFocus)
      node.removeEventListener('blur', onBlur)
    }
  }
}


code:
export const absoluteDateFormatter = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit'
})

export const shortAbsoluteDateFormatter = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'short',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit'
})


code:
export const isFullscreen = () => !!(document.fullscreenElement ||
  document.webkitFullscreenElement ||
  document.mozFullScreenElement)

export const attachFullscreenListener = (listener) => {
  if ('onfullscreenchange' in document) {
    document.addEventListener('fullscreenchange', listener)
  } else if ('onwebkitfullscreenchange' in document) {
    document.addEventListener('webkitfullscreenchange', listener)
  } else if ('onmozfullscreenchange' in document) {
    document.addEventListener('mozfullscreenchange', listener)
  }
}

export const detachFullscreenListener = (listener) => {
  if ('onfullscreenchange' in document) {
    document.removeEventListener('fullscreenchange', listener)
  } else if ('onwebkitfullscreenchange' in document) {
    document.removeEventListener('webkitfullscreenchange', listener)
  } else if ('onmozfullscreenchange' in document) {
    document.removeEventListener('mozfullscreenchange', listener)
  }
}


code:
export function getFirstIdFromItemSummaries (itemSummaries) {
  return itemSummaries &&
    itemSummaries[0] &&
    itemSummaries[0].id
}

export function getLastIdFromItemSummaries (itemSummaries) {
  return itemSummaries &&
    itemSummaries[itemSummaries.length - 1] &&
    itemSummaries[itemSummaries.length - 1].id
}


code:
// should be kept in sync with global.scss
export function getMainTopMargin () {
  if (window.matchMedia('(max-width: 767px)').matches) {
    return 62
  } else if (window.matchMedia('(max-width: 991px').matches) {
    return 52
  } else {
    return 42
  }
}


code:
/* eslint-disable */
import { thunk } from './thunk'

export const handleRegex = thunk(() => /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig)
/* eslint-enable */


code:
// hacky way to listen for pushState/replaceState changes
// per https://stackoverflow.com/a/25673911/680742

function wrapper (type) {
  let orig = history[type]
  return function () {
    let result = orig.apply(this, arguments)
    let e = new Event(type)
    e.arguments = arguments
    window.dispatchEvent(e)
    return result
  }
}

if (process.browser) {
  history.pushState = wrapper('pushState')
  history.replaceState = wrapper('replaceState')
}


code:
// Rough guess at whether this is a "mobile" device or not, for the purposes
// of "device class" estimations

let cached

export function isMobile () {
  if (typeof cached === 'undefined') {
    cached = !!(process.browser && navigator.userAgent.match(/(iPhone|iPod|iPad|Android)/))
  }
  return cached
}


code:
export function loadCSS (href) {
  let existingLink = document.querySelector(`link[href="${href}"]`)

  if (existingLink) {
    return
  }
  let link = document.createElement('link')
  link.rel = 'stylesheet'
  link.href = href

  document.head.appendChild(link)
}


code:
import {
  importCustomElementsPolyfill,
  importIndexedDBGetAllShim,
  importIntersectionObserver,
  importRequestIdleCallback
} from './asyncPolyfills'

export function loadPolyfills () {
  return Promise.all([
    typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
    typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
    !IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
    typeof customElements === 'undefined' && importCustomElementsPolyfill()
  ])
}


code:
import LoadingMask from '../_components/LoadingMask.html'

let loadingMask

if (process.browser) {
  loadingMask = new LoadingMask({
    target: document.querySelector('#loading-mask')
  })
  if (process.env.NODE_ENV !== 'production') {
    window.loadingMask = loadingMask // for debugging
  }
} else {
  loadingMask = {
  }
}

export { loadingMask }


code:
// Some functions from Lodash that are a bit heavyweight and which
// we can just do in idiomatic ES2015+

export function get (obj, keys, defaultValue) {
  for (let key of keys) {
    if (obj && key in obj) {
      obj = obj[key]
    } else {
      return defaultValue
    }
  }
  return obj
}

export function pickBy (obj, predicate) {
  let res = {}
  for (let [key, value] of Object.entries(obj)) {
    if (predicate(value, key)) {
      res[key] = value
    }
  }
  return res
}

export function padStart (string, length, chars) {
  while (string.length < length) {
    string = chars + string
  }
  return string
}


code:
const enabled = process.browser && performance.mark && (
  process.env.NODE_ENV !== 'production' ||
  (typeof location !== 'undefined' && location.search.includes('marks=true'))
)

const perf = process.browser && performance

export function mark (name) {
  if (enabled) {
    perf.mark(`start ${name}`)
  }
}

export function stop (name) {
  if (enabled) {
    perf.mark(`end ${name}`)
    perf.measure(name, `start ${name}`, `end ${name}`)
  }
}


code:
import { emojifyText } from './emojifyText'

export function massageUserText (text, emojis, $autoplayGifs) {
  text = text || ''
  text = emojifyText(text, emojis, $autoplayGifs)

  // GNU Social and Pleroma don't add <p> tags
  if (text && !text.startsWith('<p>')) {
    text = `<p>${text}</p>`
  }
  return text
}


code:
// via https://github.com/tootsuite/mastodon/blob/5d5c0f4/app/javascript/mastodon/features/compose/util/counter.js

import { urlRegex } from './urlRegex'
import { handleRegex } from './handleRegex'
import { mark, stop } from './marks'
import { length } from 'stringz'

const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'

export function measureText (inputText) {
  if (!inputText) {
    return 0
  }
  mark('measureText()')
  let normalizedText = inputText
    .replace(urlRegex(), urlPlaceholder)
    .replace(handleRegex(), '$1@$3')
  let len = length(normalizedText)
  stop('measureText()')
  return len
}


code:
// svelte helper to add a .once() method similar to .on, but only fires once

export function once (eventName, callback) {
  let listener = this.on(eventName, eventValue => {
    listener.cancel()
    callback(eventValue)
  })
}


code:
import { replaceAll } from './strings'
import { replaceEmoji } from './replaceEmoji'

export function removeEmoji (text, emojis) {
  // remove custom emoji
  if (emojis) {
    for (let emoji of emojis) {
      let shortcodeWithColons = `:${emoji.shortcode}:`
      text = replaceAll(text, shortcodeWithColons, '')
    }
  }
  // remove native emoji
  return replaceEmoji(text, () => '').trim()
}


code:
import { getEmojiRegex } from './emojiRegex'

// \ufe0f is a variation selector, which seems to appear for some reason in e.g. ™
let NON_EMOJI_REGEX = new RegExp('^[0-9#*™®\ufe0f]+$')

// replace emoji in HTML with something else, safely skipping HTML tags
export function replaceEmoji (string, replacer) {
  let output = ''
  let leftAngleBracketIdx = string.indexOf('<')
  let currentIdx = 0
  let emojiRegex = getEmojiRegex()

  function safeReplacer (substring) {
    // emoji regex matches digits and pound sign https://git.io/fpl6J
    if (substring.match(NON_EMOJI_REGEX)) {
      return substring
    }
    return replacer(substring)
  }

  while (leftAngleBracketIdx !== -1) {
    let substring = string.substring(currentIdx, leftAngleBracketIdx)

    output += substring.replace(emojiRegex, safeReplacer)

    let rightAngleBracketIdx = string.indexOf('>', leftAngleBracketIdx + 1)
    if (rightAngleBracketIdx === -1) { // broken HTML, abort
      output += string.substring(leftAngleBracketIdx, string.length)
      return output
    }
    output += string.substring(leftAngleBracketIdx, rightAngleBracketIdx) + '>'
    currentIdx = rightAngleBracketIdx + 1
    leftAngleBracketIdx = string.indexOf('<', currentIdx)
  }

  output += string.substring(currentIdx, string.length).replace(emojiRegex, safeReplacer)

  return output
}


code:
// Avoid re-renders by caching the most recent value of an array
// or an object, using an approach similar to https://github.com/reactjs/reselect.
// This avoids the issue where Svelte may keep re-rendering because it doesn't
// know if an object/array has changed or not.

import isEqual from 'lodash-es/isEqual'

if (process.browser && process.env.NODE_ENV !== 'production') {
  window.reselectStats = {}
}

export function reselect (store, outputKey, inputKey) {
  let prevValue
  let nextValue
  let count = 0
  let countKey = `${outputKey}_reselectCount`

  store.compute(countKey, [inputKey], input => {
    if (process.browser && process.env.NODE_ENV !== 'production') {
      window.reselectStats[inputKey] = window.reselectStats[inputKey] || { numInputChanges: 0, numOutputChanges: 0 }
      window.reselectStats[inputKey].numInputChanges++
    }
    if (!isEqual(prevValue, input)) {
      nextValue = input
      count++
    }
    return count
  })

  store.compute(outputKey, [countKey], () => {
    if (process.browser && process.env.NODE_ENV !== 'production') {
      window.reselectStats[inputKey].numOutputChanges++
    }
    prevValue = nextValue
    nextValue = null
    return prevValue
  })
}


code:
import debounce from 'lodash-es/debounce'

const DEBOUNCE_DELAY = 700

const listeners = new Set()

if (process.browser && process.env.NODE_ENV !== 'production') {
  window.resizeListeners = listeners
}

if (process.browser) {
  window.addEventListener('resize', debounce(() => {
    console.log('resize')
    listeners.forEach(listener => listener())
  }, DEBOUNCE_DELAY))
}

export function registerResizeListener (listener) {
  listeners.add(listener)
}

export function unregisterResizeListener (listener) {
  listeners.delete(listener)
}


code:
import { scheduleIdleTask } from './scheduleIdleTask'
import { store } from '../_store/store'
import { isMobile } from './isMobile'

// Run a task that doesn't need to be processed immediately, but should
// probably be delayed if we're on a mobile device. Also run it sooner
// if we're in a hidden tab, since browsers throttle or don't run setTimeout/rAF/etc.
export function runMediumPriorityTask (fn) {
  if (store.get().pageVisibilityHidden) {
    fn()
  } else if (isMobile()) {
    scheduleIdleTask(fn)
  } else {
    requestAnimationFrame(fn)
  }
}


code:
import localStorageMemory from 'localstorage-memory'
import { testHasLocalStorage } from './testStorage'

const safeLocalStorage = testHasLocalStorage() ? localStorage : localStorageMemory

export { safeLocalStorage }


code:
// Wrapper to call requestIdleCallback() to schedule low-priority work.
// See https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API
// for a good breakdown of the concepts behind this.

import Queue from 'tiny-queue'
import { mark, stop } from './marks'

const taskQueue = new Queue()
let runningRequestIdleCallback = false

const liteRIC = cb => setTimeout(() => cb({timeRemaining: () => Infinity})) // eslint-disable-line

function getRIC () {
  // we load polyfills asynchronously, so there's a tiny chance this is not defined
  return typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : liteRIC
}

function runTasks (deadline) {
  mark('scheduleIdleTask:runTasks()')
  while (taskQueue.length && deadline.timeRemaining() > 0) {
    let task = taskQueue.shift()
    try {
      task()
    } catch (e) {
      console.error(e)
    }
  }
  if (taskQueue.length) {
    let rIC = getRIC()
    rIC(runTasks)
  } else {
    runningRequestIdleCallback = false
  }
  stop('scheduleIdleTask:runTasks()')
}

export function scheduleIdleTask (task) {
  taskQueue.push(task)
  if (!runningRequestIdleCallback) {
    runningRequestIdleCallback = true
    let rIC = getRIC()
    rIC(runTasks)
  }
}


code:
export function getScrollContainer () {
  return document.scrollingElement
}

export function addScrollListener (listener) {
  document.addEventListener('scroll', listener)
}

export function removeScrollListener (listener) {
  document.removeEventListener('scroll', listener)
}

export function getOffsetHeight () {
  // in a subscroller, this would be element.offsetHeight, but here
  // document.scrollingElement.offsetHeight is too short for some reason.
  // This one is exact, such that scrollHeight - scrollTop - offsetHeight === 0
  // when you are scrolled to the bottom.
  return window.innerHeight
}


code:
import {
  getScrollContainer,
  getOffsetHeight } from './scrollContainer'
import { smoothScroll } from './smoothScroll'

let mainNavElement
function getTopOverlay () {
  if (!mainNavElement) {
    mainNavElement = document.getElementById('main-nav')
  }
  return mainNavElement.clientHeight
}

export function isVisible (element) {
  if (!element) {
    return false
  }
  let rect = element.getBoundingClientRect()
  let offsetHeight = getOffsetHeight()
  let topOverlay = getTopOverlay()
  return rect.top < offsetHeight && rect.bottom >= topOverlay
}

export function firstVisibleElementIndex (elements) {
  let offsetHeight = getOffsetHeight()
  let topOverlay = getTopOverlay()
  let first = -1
  let firstComplete = -1
  let len = elements.length
  let i = -1
  while (++i < len) {
    let element = elements[i]
    if (!element) {
      continue
    }
    let rect = element.getBoundingClientRect()
    if (rect.top < offsetHeight && rect.bottom >= topOverlay) {
      first = i
      firstComplete = (
        rect.top < topOverlay && i < (len - 1)) ? i + 1 : i
      break
    }
  }
  return { first, firstComplete }
}

export function scrollIntoViewIfNeeded (element) {
  let rect = element.getBoundingClientRect()
  let topOverlay = getTopOverlay()
  let offsetHeight = getOffsetHeight()
  let scrollY = 0
  if (rect.top < topOverlay) {
    scrollY = topOverlay
  } else if (rect.bottom > offsetHeight) {
    let height = rect.bottom - rect.top
    if ((offsetHeight - topOverlay) > height) {
      scrollY = offsetHeight - height
    } else {
      // if element height is too great to fit,
      // prefer showing the top of the element
      scrollY = topOverlay
    }
  } else {
    return // not needed
  }
  let scrollContainer = getScrollContainer()
  let scrollTop = scrollContainer.scrollTop
  smoothScroll(scrollContainer, scrollTop + rect.top - scrollY)
}


code:
import { getScrollContainer } from './scrollContainer'
import { smoothScroll } from './smoothScroll'

export function scrollToTop (smooth) {
  let scroller = getScrollContainer()
  let { scrollTop } = scroller
  if (scrollTop === 0) {
    return false
  }
  if (smooth) {
    smoothScroll(scroller, 0)
  } else {
    scroller.scrollTop = 0
  }
  return true
}


code:
import { snackbar } from '../_components/snackbar/snackbar'

async function skipWaiting () {
  const reg = await navigator.serviceWorker.getRegistration()
  if (!reg || !reg.waiting) {
    return
  }
  reg.waiting.postMessage('skip-waiting')
}

function onUpdateFound (registration) {
  const newWorker = registration.installing

  newWorker.addEventListener('statechange', async () => {
    if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
      snackbar.announce('App update available.', 'Reload', async () => {
        await skipWaiting()
        document.location.reload(true)
      })
    }
  })
}

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js').then(registration => {
    registration.addEventListener('updatefound', () => onUpdateFound(registration))
  })
}


code:
// borrowed from https://github.com/HenrikJoreteg/favicon-setter
export function setFavicon (href) {
  let faviconId = 'theFavicon'
  let oldLink = document.getElementById(faviconId)

  if (oldLink.getAttribute('href') === href) {
    return
  }

  let link = document.createElement('link')
  link.id = faviconId
  link.rel = 'shortcut icon'
  link.type = 'image/png'
  link.href = href
  document.head.removeChild(oldLink)
  document.head.appendChild(link)
}


code:
import { store } from '../_store/store'

// A map of scopeKey to KeyMap
let scopeKeyMaps

// Current scope, starting with 'global'
let currentScopeKey

// Previous current scopes
let scopeStack

// Currently active prefix map
let prefixMap

// Scope in which prefixMap is valid
let prefixMapScope

// A map of key to components or other KeyMaps
function KeyMap () {}

export function initShortcuts () {
  currentScopeKey = 'global'
  scopeStack = []
  scopeKeyMaps = null
  prefixMap = null
  prefixMapScope = null
}
initShortcuts()

// Sets scopeKey as current scope.
export function pushShortcutScope (scopeKey) {
  scopeStack.push(currentScopeKey)
  currentScopeKey = scopeKey
}

// Go back to previous current scope.
export function popShortcutScope (scopeKey) {
  if (scopeKey !== currentScopeKey) {
    return
  }
  currentScopeKey = scopeStack.pop()
}

// Call component.onKeyDown(event) when a key in keys is pressed
// in the given scope.
export function addToShortcutScope (scopeKey, keys, component) {
  if (scopeKeyMaps == null) {
    window.addEventListener('keydown', onKeyDown)
    scopeKeyMaps = {}
  }
  let keyMap = scopeKeyMaps[scopeKey]
  if (!keyMap) {
    keyMap = new KeyMap()
    scopeKeyMaps[scopeKey] = keyMap
  }
  mapKeys(keyMap, keys, component)
}

export function removeFromShortcutScope (scopeKey, keys, component) {
  let keyMap = scopeKeyMaps[scopeKey]
  if (!keyMap) {
    return
  }
  unmapKeys(keyMap, keys, component)
  if (Object.keys(keyMap).length === 0) {
    delete scopeKeyMaps[scopeKey]
  }
  if (Object.keys(scopeKeyMaps).length === 0) {
    scopeKeyMaps = null
    window.removeEventListener('keydown', onKeyDown)
  }
}

const FALLBACK_KEY = '__fallback__'

// Call component.onKeyDown(event) if no other shortcuts handled
// the current key.
export function addShortcutFallback (scopeKey, component) {
  addToShortcutScope(scopeKey, FALLBACK_KEY, component)
}

export function removeShortcutFallback (scopeKey, component) {
  removeFromShortcutScope(scopeKey, FALLBACK_KEY, component)
}

// Direct the given event to the appropriate component in the given
// scope for the event's key.
export function onKeyDownInShortcutScope (scopeKey, event) {
  if (prefixMap) {
    let handled = false
    if (prefixMap && prefixMapScope === scopeKey) {
      handled = handleEvent(scopeKey, prefixMap, event.key, event)
    }
    prefixMap = null
    prefixMapScope = null
    if (handled) {
      return
    }
  }
  let keyMap = scopeKeyMaps[scopeKey]
  if (!keyMap) {
    return
  }
  if (!handleEvent(scopeKey, keyMap, event.key, event)) {
    handleEvent(scopeKey, keyMap, FALLBACK_KEY, event)
  }
}

function handleEvent (scopeKey, keyMap, key, event) {
  let value = keyMap[key]
  if (!value) {
    return false
  }
  if (KeyMap.prototype.isPrototypeOf(value)) {
    prefixMap = value
    prefixMapScope = scopeKey
  } else {
    value.onKeyDown(event)
  }
  return true
}

function onKeyDown (event) {
  if (store.get().disableHotkeys) {
    return
  }
  if (!acceptShortcutEvent(event)) {
    return
  }
  onKeyDownInShortcutScope(currentScopeKey, event)
}

function mapKeys (keyMap, keys, component) {
  keys.split('|').forEach(
    (seq) => {
      let seqArray = seq.split(' ')
      let prefixLen = seqArray.length - 1
      let currentMap = keyMap
      let i = -1
      while (++i < prefixLen) {
        let prefixMap = currentMap[seqArray[i]]
        if (!prefixMap) {
          prefixMap = new KeyMap()
          currentMap[seqArray[i]] = prefixMap
        }
        currentMap = prefixMap
      }
      currentMap[seqArray[prefixLen]] = component
    })
}

function unmapKeys (keyMap, keys, component) {
  keys.split('|').forEach(
    (seq) => {
      let seqArray = seq.split(' ')
      let prefixLen = seqArray.length - 1
      let currentMap = keyMap
      let i = -1
      while (++i < prefixLen) {
        let prefixMap = currentMap[seqArray[i]]
        if (!prefixMap) {
          return
        }
        currentMap = prefixMap
      }
      let lastKey = seqArray[prefixLen]
      if (currentMap[lastKey] === component) {
        delete currentMap[lastKey]
      }
    })
}

function acceptShortcutEvent (event) {
  let { target } = event
  return !(
    event.altKey ||
    event.metaKey ||
    event.ctrlKey ||
    (event.shiftKey && event.key !== '?') || // '?' is a special case - it is allowed
    (target && (
      target.isContentEditable ||
        ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)
    ))
  )
}


code:
import { store } from '../_store/store'

// via https://github.com/tootsuite/mastodon/blob/f59ed3a4fafab776b4eeb92f805dfe1fecc17ee3/app/javascript/mastodon/scroll.js
const easingOutQuint = (x, t, b, c, d) =>
  c * ((t = t / d - 1) * t * t * t * t + 1) + b

function smoothScrollPolyfill (node, key, target) {
  const startTime = Date.now()
  const offset = node[key]
  const gap = target - offset
  const duration = 1000
  let interrupt = false

  const step = () => {
    const elapsed = Date.now() - startTime
    const percentage = elapsed / duration

    if (interrupt) {
      return
    }

    if (percentage > 1) {
      cleanup()
      return
    }

    node[key] = easingOutQuint(0, elapsed, offset, gap, duration)
    requestAnimationFrame(step)
  }

  const cancel = () => {
    interrupt = true
    cleanup()
  }

  const cleanup = () => {
    node.removeEventListener('wheel', cancel)
    node.removeEventListener('touchstart', cancel)
  }

  node.addEventListener('wheel', cancel, { passive: true })
  node.addEventListener('touchstart', cancel, { passive: true })

  step()

  return cancel
}

function testSupportsSmoothScroll () {
  let supports = false
  try {
    let div = document.createElement('div')
    div.scrollTo({
      top: 0,
      get behavior () {
        supports = true
        return 'smooth'
      }
    })
  } catch (err) {} // Edge throws an error
  return supports
}

export const hasNativeSmoothScroll = process.browser && testSupportsSmoothScroll()

export function smoothScroll (node, topOrLeft, horizontal) {
  if (store.get().reduceMotion) {
    // don't do smooth-scroll at all for users who prefer reduced motion
    node[horizontal ? 'scrollLeft' : 'scrollTop'] = topOrLeft
  } else if (hasNativeSmoothScroll) {
    return node.scrollTo({
      [horizontal ? 'left' : 'top']: topOrLeft,
      behavior: 'smooth'
    })
  } else {
    return smoothScrollPolyfill(node, horizontal ? 'scrollLeft' : 'scrollTop', topOrLeft)
  }
}


code:
import { mark, stop } from './marks'

let domParser = process.browser && new DOMParser()

// mentions like "@foo" have to be expanded to "@foo@example.com"
function massageMentions (doc, mentions) {
  let anchors = doc.querySelectorAll('a.mention')
  for (let i = 0; i < anchors.length; i++) {
    let anchor = anchors[i]
    let href = anchor.getAttribute('href')
    let mention = mentions.find(mention => mention.url === href)
    if (mention) {
      anchor.innerText = `@${mention.acct}`
    }
  }
}

// paragraphs should be separated by double newlines
// single <br/>s should become single newlines
function innerTextRetainingNewlines (doc) {
  let res = ''
  let paragraphs = doc.querySelectorAll('p')
  for (let i = 0; i < paragraphs.length; i++) {
    let paragraph = paragraphs[i]
    let brs = paragraph.querySelectorAll('br')
    for (let j = 0; j < brs.length; j++) {
      let br = brs[j]
      br.parentNode.replaceChild(doc.createTextNode('\n'), br)
    }
    res += (i > 0 ? '\n\n' : '') + paragraph.textContent
  }
  return res
}

export function statusHtmlToPlainText (html, mentions) {
  if (!html) {
    return ''
  }
  mark('statusHtmlToPlainText')
  // GNU Social and Pleroma don't add <p> tags
  if (!html.startsWith('<p>')) {
    html = `<p>${html}</p>`
  }
  let doc = domParser.parseFromString(html, 'text/html')
  massageMentions(doc, mentions)
  let res = innerTextRetainingNewlines(doc)
  stop('statusHtmlToPlainText')
  return res
}


code:
// Pleroma uses base62 IDs, Mastodon uses 0-9 big ints encoded as strings.
// Using base62 for both works, since the first 10 characters of base62
// are 0-9.

import { padStart } from './lodash-lite'

// Pleroma uses the 0-9A-Za-z alphabet for base62, which is the same as ASCII, which
// is the same as JavaScript sort order and IndexedDB order.
const MIN_CHAR_CODE = 48 // '0'.charCodeAt(0)
const MAX_CHAR_CODE = 122 // 'z'.charCodeAt(0)
const MAX_ID_LENGTH = 30 // assume that Mastodon/Pleroma IDs won't get any bigger than this

export function zeroPad (str, toSize) {
  return padStart(str, toSize, '0')
}

export function toPaddedBigInt (id) {
  return zeroPad(id, MAX_ID_LENGTH)
}

export function toReversePaddedBigInt (id) {
  let padded = toPaddedBigInt(id)
  let reversed = ''
  for (let i = 0; i < padded.length; i++) {
    let charCode = padded.charCodeAt(i)
    let inverseCharCode = MIN_CHAR_CODE + MAX_CHAR_CODE - charCode
    reversed += String.fromCharCode(inverseCharCode)
  }
  return reversed
}

export function compareTimelineItemSummaries (left, right) {
  let leftPadded = toPaddedBigInt(left.id)
  let rightPadded = toPaddedBigInt(right.id)
  return leftPadded < rightPadded ? -1 : leftPadded === rightPadded ? 0 : 1
}


code:
export function replaceAll (string, replacee, replacement) {
  if (!string.length || !replacee.length) {
    return string
  }
  let idx
  let pos = 0
  while (true) {
    idx = string.indexOf(replacee, pos)
    if (idx === -1) {
      break
    }
    string = string.substring(0, idx) + replacement + string.substring(idx + replacee.length)
    pos = idx + replacement.length
  }
  return string
}


code:
// Hit both the cache and the network, setting state for the cached version first,
// then the network version (as it's assumed to be fresher). Also update the db afterwards.
export async function cacheFirstUpdateAfter (networkFetcher, dbFetcher, dbUpdater, stateSetter) {
  let networkPromise = networkFetcher() // kick off network request immediately
  let dbResponse
  try {
    dbResponse = await dbFetcher()
  } catch (err) {
    console.error('ignored DB error', err)
  } finally {
    if (dbResponse) {
      stateSetter(dbResponse)
    }
    let fetchAndUpdatePromise = networkPromise.then(networkResponse => {
      /* no await */ dbUpdater(networkResponse)
      stateSetter(networkResponse)
    })
    if (!dbResponse) { // no cached result available, await the network
      await fetchAndUpdatePromise
    }
  }
}


code:
// LocalStorage and IDB may be disabled in private mode, when "blocking cookies" in Safari,
// or other cases

import { thunk } from './thunk'

const testKey = '__test__'

export const testHasLocalStorageOnce = () => {
  try {
    localStorage.setItem(testKey, testKey)
    if (!localStorage.length || localStorage.getItem(testKey) !== testKey) {
      return false
    }
    localStorage.removeItem(testKey)
  } catch (e) {
    return false
  }
  return true
}

export const testHasLocalStorage = thunk(testHasLocalStorageOnce)

export const testHasIndexedDB = thunk(async () => {
  if (typeof indexedDB === 'undefined') {
    return false
  }

  try {
    let idbFailed = await new Promise(resolve => {
      let db = indexedDB.open(testKey)
      db.onerror = () => resolve(true)
      db.onsuccess = () => {
        indexedDB.deleteDatabase(testKey)
        resolve(false)
      }
    })
    if (idbFailed) {
      return false
    }
  } catch (e) {
    return false
  }
  return true
})


code:
let meta = process.browser && document.getElementById('theThemeColor')
let offlineStyle = process.browser && document.getElementById('theOfflineStyle')
let prefersDarkTheme = process.browser && window.matchMedia('(prefers-color-scheme: dark)').matches

export const INLINE_THEME = 'default' // theme that does not require external CSS
export const DEFAULT_LIGHT_THEME = 'default' // theme that is shown by default
export const DEFAULT_DARK_THEME = 'ozark' // theme that is shown for prefers-color-scheme:dark
export const DEFAULT_THEME = prefersDarkTheme ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME

function getExistingThemeLink () {
  return document.head.querySelector('link[rel=stylesheet][href^="/theme-"]')
}

function resetExistingTheme () {
  let existingLink = getExistingThemeLink()
  if (existingLink) {
    document.head.removeChild(existingLink)
  }
}

function loadCSS (href) {
  let existingLink = getExistingThemeLink()

  let link = document.createElement('link')
  link.rel = 'stylesheet'
  link.href = href

  link.addEventListener('load', function onload () {
    link.removeEventListener('load', onload)
    if (existingLink) { // remove after load to avoid flash of default theme
      document.head.removeChild(existingLink)
    }
  })

  // inserting before the offline <style> ensures that the offline style wins when offline
  document.head.insertBefore(link, offlineStyle)
}

export function switchToTheme (themeName = DEFAULT_THEME) {
  let themeColor = window.__themeColors[themeName]
  meta.content = themeColor || window.__themeColors[DEFAULT_THEME]
  if (themeName !== INLINE_THEME) {
    loadCSS(`/theme-${themeName}.css`)
  } else {
    resetExistingTheme()
  }
}


code:
export function thunk (func) {
  let cached
  let runOnce
  return () => {
    if (!runOnce) {
      cached = func()
      runOnce = true
    }
    return cached
  }
}


code:
export function timelineItemToSummary (item) {
  return {
    id: item.id,
    replyId: (item.in_reply_to_id) || void 0,
    reblogId: (item.reblog && item.reblog.id) || void 0,
    type: item.type || void 0
  }
}


code:
import { thunk } from './thunk'

export const urlRegex = thunk(() => {
  // this is provided at build time to avoid having a lot of runtime code just to build
  // a static regex
  return process.env.URL_REGEX
})


code:
// via https://github.com/tootsuite/mastodon/blob/995f8b3/app/javascript/mastodon/features/compose/util/url_regex.js

/* eslint-disable */

module.exports = () => {
  const regexen = {};

  const regexSupplant = function(regex, flags) {
    flags = flags || '';
    if (typeof regex !== 'string') {
      if (regex.global && flags.indexOf('g') < 0) {
        flags += 'g';
      }
      if (regex.ignoreCase && flags.indexOf('i') < 0) {
        flags += 'i';
      }
      if (regex.multiline && flags.indexOf('m') < 0) {
        flags += 'm';
      }

      regex = regex.source;
    }
    return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
      var newRegex = regexen[name] || '';
      if (typeof newRegex !== 'string') {
        newRegex = newRegex.source;
      }
      return newRegex;
    }), flags);
  };

  const stringSupplant = function(str, values) {
    return str.replace(/#\{(\w+)\}/g, function(match, name) {
      return values[name] || '';
    });
  };
  regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
  regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
  regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
  regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@＠$#＃#{invalid_chars_group}]|^)/);
  regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen);
  regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
  regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
  regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
  regexen.validGTLD = regexSupplant(RegExp(
    '(?:(?:' +
    '삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
    '政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
    'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
    'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
    'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
    'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
    'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
    'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
    'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
    'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
    'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
    'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
    'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
    'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
    'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
    'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
    'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
    'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
    'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
    'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
    'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
    'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
    'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
    'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
    'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
    'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
    'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
    'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
    'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
    'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
    'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
    'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
    'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
    'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
    'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
    'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
    'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
    'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
    'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
    'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
    'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
    'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
    'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
    'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
    'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
    'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
    'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
    'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
    'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
    'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
    'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
    'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
    'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
    'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
    'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
    'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
    'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
    'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
    'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
    'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
    'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
    'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
    'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
    'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
    'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
    'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
    'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
    'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
    'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
    'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
    'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
    'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
    'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
    'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
    'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
    'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
    'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
    'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
    'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
    'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
    'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
    'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
    'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
    'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
    'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
    'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
    'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
    'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
    ')(?=[^0-9a-zA-Z@]|$))'));
  regexen.validCCTLD = regexSupplant(RegExp(
    '(?:(?:' +
    '한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
    'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
    'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
    'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
    'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
    're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
    'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
    'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
    'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
    'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
    'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
    ')(?=[^0-9a-zA-Z@]|$))'));
  regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
  regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
  regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
  regexen.validPortNumber = /[0-9]+/;
  regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/;
  regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i);
  // Allow URL paths to contain up to two nested levels of balanced parens
  //  1. Used in Wikipedia URLs like /Primer_(film)
  //  2. Used in IIS sessions like /S(dfd346)/
  //  3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
  regexen.validUrlBalancedParens = regexSupplant(
    '\\('                                   +
    '(?:'                                 +
    '#{validGeneralUrlPathChars}+'      +
    '|'                                 +
    // allow one nested level of balanced parentheses
    '(?:'                               +
    '#{validGeneralUrlPathChars}*'    +
    '\\('                             +
    '#{validGeneralUrlPathChars}+'  +
    '\\)'                             +
    '#{validGeneralUrlPathChars}*'    +
    ')'                                 +
    ')'                                   +
    '\\)',
    'i');
  // Valid end-of-path characters (so /foo. does not gobble the period).
  // 1. Allow =&# for empty URL parameters and other URL-join artifacts
  regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
  // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
  regexen.validUrlPath = regexSupplant('(?:' +
    '(?:' +
    '#{validGeneralUrlPathChars}*' +
    '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
    '#{validUrlPathEndingChars}'+
    ')|(?:@#{validGeneralUrlPathChars}+\/)'+
    ')', 'i');
  regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
  regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
  regexen.validUrl = regexSupplant(
    '('                                                          + // $1 URL
    '(https?:\\/\\/)'                                          + // $2 Protocol
    '(#{validDomain})'                                         + // $3 Domain(s)
    '(?::(#{validPortNumber}))?'                               + // $4 Port number (optional)
    '(\\/#{validUrlPath}*)?'                                   + // $5 URL Path
    '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?'  + // $6 Query String
    ')',
    'gi');
  return regexen.validUrl;
}

/* eslint-enable */


code:
<Title name="Followers" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/accounts/[accountId]/followers.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Follows" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/accounts/[accountId]/follows.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Profile" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/accounts/[accountId]/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Profile with media" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/accounts/[accountId]/media.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Profile with replies" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/accounts/[accountId]/with_replies.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Blocked users" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/blocked.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Community" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/community/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Direct messages" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/direct.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Favorites" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/favorites.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Federated" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/federated.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Home" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="List" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/lists/[listId].html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Local" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/local.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Muted users" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/muted.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Notifications" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/notifications/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Notifications" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/notifications/mentions.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Pinned toots" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/pinned.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Follow requests" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/requests.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Search" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from './_components/Title.html'
  import LazyPage from './_components/LazyPage.html'
  import pageComponent from './_pages/search.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="About" settingsPage={true} />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/settings/about.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="General Settings" settingsPage={true} />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/settings/general.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Hotkeys" settingsPage={true} />

<LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/settings/hotkeys.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Settings" settingsPage={true} />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/settings/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="{params.instanceName}" settingsPage={true} />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/settings/instances/[instanceName].html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Add instance" settingsPage={true} />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/settings/instances/add.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Instances" settingsPage={true} />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/settings/instances/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<!-- this is only used in the tests, to log in quickly -->
<script>
  import { store } from '../_store/store'
  import { goto } from '../../../__sapper__/client'

  export default {
    store: () => store,
    oncreate () {
      let accessToken = location.search.match(/accessToken=([^&]+)/)[1]
      let instanceName = location.search.match(/instanceName=([^&]+)/)[1]
      let {
        loggedInInstances,
        loggedInInstancesInOrder
      } = this.store.get()

      loggedInInstances[instanceName] = {
        access_token: accessToken
      }

      if (!loggedInInstancesInOrder.includes(instanceName)) {
        loggedInInstancesInOrder.push(instanceName)
      }

      this.store.set({
        currentInstance: instanceName,
        loggedInInstances,
        loggedInInstancesInOrder
      })
      this.store.save()
      goto('/')
    }
  }
</script>


code:
<!-- this is just used for the web share target API -->
<script>
  import { store } from './_store/store'
  import { goto } from '../../__sapper__/client'
  import { decodeURIComponentWithPluses } from './_utils/decodeURIComponentWithPluses'

  const SHARE_KEYS = ['title', 'text', 'url']

  export default {
    store: () => store,
    oncreate () {
      let params = new URLSearchParams(location.search)

      let text = SHARE_KEYS
        .map(key => params.get(key) && decodeURIComponentWithPluses(params.get(key)))
        .filter(Boolean)
        .join(' ')

      this.store.set({ openShareDialog: true })
      this.store.clearComposeData('dialog')
      this.store.setComposeData('dialog', { text })
      this.store.save()

      goto('/', { replaceState: true })
    }
  }
</script>


code:
<Title name="Favorites" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/statuses/[statusId]/favorites.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Status" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/statuses/[statusId]/index.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Reblogs" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../../_components/Title.html'
  import LazyPage from '../../_components/LazyPage.html'
  import pageComponent from '../../_pages/statuses/[statusId]/reblogs.html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
<Title name="Hashtag" />

  <LazyPage {pageComponent} {params} />

<script>
  import Title from '../_components/Title.html'
  import LazyPage from '../_components/LazyPage.html'
  import pageComponent from '../_pages/tags/[tagName].html'

  export default {
    components: {

      Title,
      LazyPage
    },
    data: () => ({
      pageComponent
    })
  }
</script>


code:
html {
  // Firefox with scrollbar config changes as of 2018-11
  scrollbar-face-color: var(--scrollbar-face-color);
  scrollbar-track-color: var(--scrollbar-track-color);
  // Firefox nightly as of 2018-11, new standard version
  // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
  scrollbar-color: var(--scrollbar-face-color) var(--scrollbar-track-color);
}

::-webkit-scrollbar {
  width: var(--scrollbar-width);
  height: var(--scrollbar-height);
}

::-webkit-scrollbar-thumb {
  background: var(--scrollbar-face-color);
  border-radius: var(--scrollbar-border-radius);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-face-color-hover);
}

::-webkit-scrollbar-thumb:active {
  background: var(--scrollbar-face-color-active);
}

::-webkit-scrollbar-track, ::-webkit-scrollbar-track:hover, ::-webkit-scrollbar-track:active {
  background: var(--scrollbar-track-color);
}

::-webkit-scrollbar-corner {
  background: var(--scrollbar-background-color);
}


code:
@font-face {
  font-family: PinaforeRegular;
  src: local("BlinkMacSystemFont"),
    local("Segoe UI"),
    local("Roboto"),
    local("Oxygen-Sans"),
    local("Ubuntu"),
    local("Cantarell"),
    local("Fira Sans"),
    local("Droid Sans"),
    local("Helvetica Neue");
}

@font-face {
  font-family: PinaforeEmoji;
  src: local("Apple Color Emoji"),
    local("Segoe UI Emoji"),
    local("Segoe UI Symbol"),
    local("Twemoji Mozilla"),
    local("Noto Color Emoji"),
    local("EmojiOne Color"),
    local("Android Emoji");
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, PinaforeRegular, sans-serif;
  font-size: 14px;
  line-height: 1.4;
  color: var(--body-text-color);
  background: var(--body-bg);
  -webkit-tap-highlight-color: transparent; // fix for blue background on spoiler tap on Chrome for Android
  overflow-x: hidden; // Prevent horizontal scrolling on mobile Firefox on small screens
}

.main-content {
  contain: content; // see https://www.w3.org/TR/2018/CR-css-contain-1-20181108/#valdef-contain-content
  // these paddings should be kept in sync with getMainTopMargin.js
  @supports (-webkit-overflow-scrolling: touch) {
    // fixes iOS Safari horizontal scrolling (see https://github.com/nolanlawson/pinafore/issues/667)
    overflow-x: hidden;
  }
  padding-top: 42px;
  @media (max-width: 991px) {
    padding-top: 52px;
  }
  @media (max-width: 767px) {
    padding-top: 62px;
  }
}

main {
  position: relative;
  width: 602px;
  // take into account scrollbars;
  // https://stackoverflow.com/a/34884924
  max-width: 100%;
  padding: 0;
  box-sizing: border-box;
  margin: 30px auto 15px;
  background: var(--main-bg);
  border: 1px solid var(--main-border);
  border-radius: 1px;
  min-height: 70vh;
  @media (max-width: 767px) {
    margin: 5px auto 15px;
  }
}

footer {
  width: 602px;
  max-width: 100vw;
  box-sizing: border-box;
  margin: 15px auto;
  border-radius: 1px;
  background: var(--main-bg);
  font-size: 0.9em;
  padding: 20px;
  border: 1px solid var(--main-border);
}

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 0.5em 0;
  font-weight: 400;
  line-height: 1.2;
}

h1 {
  font-size: 2em;
}

a {
  color: var(--anchor-text);
  text-decoration: none;

  &:visited {
    color: var(--anchor-text);
  }

  &:hover {
    text-decoration: underline;
  }

}

input {
  border: 1px solid var(--input-border);
  padding: 5px;
  box-sizing: border-box;
}

input[type=search] {
  -webkit-appearance: none; // reset Safari user agent stylesheet
}

// Fixes gray/black background when using a dark GTK theme
input, textarea {
  background: inherit;
  color: inherit;
}

textarea {
  font-family: system-ui, -apple-system, PinaforeRegular, sans-serif, PinaforeEmoji;
}

button, .button {
  font-size: 1.2em;
  background: var(--button-bg);
  border-radius: 2px;
  padding: 10px 15px;
  border: 1px solid var(--button-border);
  cursor: pointer;
  color: var(--button-text);

  &:hover {
    background: var(--button-bg-hover);
    text-decoration: none;
  }

  &:active {
    background: var(--button-bg-active);
  }

  &[disabled] {
    opacity: 0.35;
    pointer-events: none;
    cursor: not-allowed;
  }

  &.primary {
    border: 1px solid var(--button-primary-border);
    background: var(--button-primary-bg);
    color: var(--button-primary-text);

    &:hover {
      background: var(--button-primary-bg-hover);
    }

    &:active {
      background: var(--button-primary-bg-active);
    }
  }
}

p, label, input {
  font-size: 1.3em;
}

ul, li, p {
  padding: 0;
  margin: 0;
}

.hidden {
  opacity: 0;
}

*:focus, .focus {
  outline: 2px solid var(--focus-outline);
}

.container:focus {
  // the outline causes choppy rendering on Edge and isn't visible or necessary anyway
  // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/17343598/
  outline: none;
}

button::-moz-focus-inner {
  border: 0;
}

/* Firefox hacks to remove ugly red border.
   Unnecessary since it gives a warning if you submit an empty field anyway. */
input:required, input:invalid {
  box-shadow: none;
}

textarea {
  font-family: inherit;
  font-size: inherit;
  box-sizing: border-box;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  25% {
    transform: rotate(90deg);
  }
  50% {
    transform: rotate(180deg);
  }
  75% {
    transform: rotate(270deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.spin {
  animation: spin 1.5s infinite linear;
}

.ellipsis::after {
  content: "\2026";
}

/* via https://stackoverflow.com/a/19758620 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

/* this gets injected as raw HTML, so it's easiest to just define it in global.scss */
.inline-custom-emoji {
  width: 1.4em;
  height: 1.4em;
  margin: -0.1em 0;
  object-fit: contain;
  vertical-align: middle;
}

.inline-emoji {
  font-family: PinaforeEmoji, sans-serif;
}


code:
:root {
  $deemphasized-color: #666;

  --button-primary-bg: #{$main-theme-color};
  --button-primary-text: #{$secondary-text-color};
  --button-primary-border: #{darken($main-theme-color, 10%)};
  --button-primary-bg-active: #{lighten($main-theme-color, 9%)};
  --button-primary-bg-hover: #{lighten($main-theme-color, 5%)};

  --button-bg: #{darken($main-bg-color, 10%)};
  --button-text: #{$main-text-color};
  --button-border: #{darken($border-color, 20%)};
  --button-bg-active: #{darken($main-bg-color, 25%)};
  --button-bg-hover: #{darken($main-bg-color, 5%)};

  --input-border: #{$border-color};
  --input-bg: #{$main-bg-color};
  --anchor-text: #{$anchor-color};
  --main-bg: #{$main-bg-color};
  --body-bg: #{$body-bg-color};
  --body-text-color: #{$main-text-color};
  --main-border: #{$border-color};
  --svg-fill: #{$main-theme-color};

  --form-bg: #{darken($main-bg-color, 3%)};
  --form-border: #{darken($border-color, 10%)};

  --nav-bg: #{$main-theme-color};
  --nav-active-bg: #{lighten($main-theme-color, 9%)};
  --nav-border: #{darken($main-theme-color, 10%)};
  --nav-a-border: #{$main-theme-color};
  --nav-a-selected-border: #{$secondary-text-color};
  --nav-a-selected-bg: #{lighten($main-theme-color, 10%)};
  --nav-a-selected-active-bg: #{lighten($main-theme-color, 17%)};
  --nav-svg-fill: #{$secondary-text-color};
  --nav-text-color: #{$secondary-text-color};

  --nav-a-selected-border-hover: #{$secondary-text-color};
  --nav-a-selected-bg-hover: #{lighten($main-theme-color, 15%)};
  --nav-a-bg-hover: #{lighten($main-theme-color, 5%)};
  --nav-a-border-hover: #{$main-theme-color};
  --nav-svg-fill-hover: #{$secondary-text-color};
  --nav-text-color-hover: #{$secondary-text-color};

  --action-button-fill-color: #{lighten($main-theme-color, 18%)};
  --action-button-fill-color-hover: #{lighten($main-theme-color, 22%)};
  --action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
  --action-button-fill-color-pressed: #{darken($main-theme-color, 7%)};
  --action-button-fill-color-pressed-hover: #{darken($main-theme-color, 2%)};
  --action-button-fill-color-pressed-active: #{darken($main-theme-color, 15%)};

  --action-button-deemphasized-fill-color: #{$deemphasized-color};
  --action-button-deemphasized-fill-color-hover: #{lighten($deemphasized-color, 22%)};
  --action-button-deemphasized-fill-color-active: #{lighten($deemphasized-color, 5%)};
  --action-button-deemphasized-fill-color-pressed: #{darken($deemphasized-color, 7%)};
  --action-button-deemphasized-fill-color-pressed-hover: #{darken($deemphasized-color, 2%)};
  --action-button-deemphasized-fill-color-pressed-active: #{darken($deemphasized-color, 15%)};

  --settings-list-item-bg: #{$main-bg-color};
  --settings-list-item-text: #{$main-theme-color};
  --settings-list-item-text-hover: #{$main-theme-color};
  --settings-list-item-border: #{$border-color};
  --settings-list-item-bg-active: #{darken($main-bg-color, 10%)};
  --settings-list-item-bg-hover: #{darken($main-bg-color, 2%)};

  --toast-bg: #{$toast-bg};
  --toast-border: #{$toast-border};
  --toast-text: #{$secondary-text-color};
  --toast-button-hover: #{lighten($toast-bg, 5%)};
  --toast-button-active: #{lighten($toast-bg, 10%)};
  --toast-anchor-color: #{lighten($anchor-color, 20%)};

  --mask-bg: #{$toast-bg};
  --mask-svg-fill: #{$secondary-text-color};
  --mask-opaque-bg: #{rgba($toast-bg, 0.8)};
  --loading-bg: #{#ededed};

  --account-profile-bg-backdrop-filter: #{rgba($main-bg-color, 0.7)};
  --account-profile-bg: #{rgba($main-bg-color, 0.9)};

  --deemphasized-text-color: #{$deemphasized-color};
  --focus-outline: #{$focus-outline};

  --very-deemphasized-link-color: #{rgba($anchor-color, 0.6)};
  --very-deemphasized-text-color: #{rgba(#666, 0.6)};

  --status-direct-background: #{darken($body-bg-color, 5%)};
  --main-theme-color: #{$main-theme-color};
  --warning-color: #{#e01f19};
  --alt-input-bg: #{rgba($main-bg-color, 0.9)};

  --muted-modal-text: #{$secondary-text-color};
  --muted-modal-bg: #{transparent};
  --muted-modal-focus: #{#999};
  --muted-modal-hover: #{rgba(255, 255, 255, 0.2)};

  --compose-autosuggest-item-hover: #{$compose-background};
  --compose-autosuggest-item-active: #{darken($compose-background, 5%)};
  --compose-autosuggest-outline: #{lighten($focus-outline, 5%)};

  --compose-button-halo: #{rgba(255, 255, 255, 0.1)};

  --file-drop-mask: #{rgba(255, 255, 255, 0.8)};

  --banner-fill: #{$main-theme-color};

  --tab-bg: #{$main-bg-color};
  --tab-bg-non-selected: #{darken($main-bg-color, 3%)};
  --tab-bg-active: #{darken($main-bg-color, 25%)};
  --tab-bg-hover: #{darken($main-bg-color, 2%)};
  --tab-bg-hover-non-selected: #{darken($main-bg-color, 5%)};

  --tooltip-bg: rgba(30, 30, 30, 0.9);
  --tooltip-color: white;

  --floating-button-bg: #{rgba($main-bg-color, 0.8)};
  --floating-button-bg-hover: #{darken(rgba($main-bg-color, 0.9), 5%)};
  --floating-button-bg-active: #{darken(rgba($main-bg-color, 0.9), 10%)};

  --length-indicator-color: #{$main-theme-color};
}


code:
:root {
  $deemphasized-color: lighten($main-bg-color, 45%);

  --action-button-deemphasized-fill-color: #{$deemphasized-color};
  --action-button-deemphasized-fill-color-hover: #{lighten($deemphasized-color, 22%)};
  --action-button-deemphasized-fill-color-active: #{lighten($deemphasized-color, 5%)};
  --action-button-deemphasized-fill-color-pressed: #{darken($deemphasized-color, 7%)};
  --action-button-deemphasized-fill-color-pressed-hover: #{darken($deemphasized-color, 2%)};
  --action-button-deemphasized-fill-color-pressed-active: #{darken($deemphasized-color, 15%)};

  --loading-bg: #{#222};

  --deemphasized-text-color: #{$deemphasized-color};

  --very-deemphasized-link-color: #{rgba($anchor-color, 0.8)};
  --very-deemphasized-text-color: #{lighten($main-bg-color, 32%)};

  --status-direct-background: #{darken($body-bg-color, 5%)};
  --main-theme-color: #{$main-theme-color};
  --warning-color: #{#c7423d};
  --alt-input-bg: #{rgba($main-bg-color, 0.9)};

  --muted-modal-bg: #{transparent};
  --muted-modal-focus: #{#999};
  --muted-modal-hover: #{rgba(255, 255, 255, 0.2)};

  --compose-button-halo: #{rgba(0, 0, 0, 0.1)};

  --compose-autosuggest-item-hover: #{lighten($main-bg-color, 10%)};
  --compose-autosuggest-item-active: #{lighten($main-bg-color, 15%)};
  --compose-autosuggest-outline: #{lighten($border-color, 20%)};

  --file-drop-mask: #{rgba(30, 30, 30, 0.8)};

  --settings-list-item-text: #{$main-text-color};
  --settings-list-item-text-hover: #{$main-text-color};
  --settings-list-item-bg-active: #{lighten($main-bg-color, 7%)};
  --settings-list-item-bg-hover: #{lighten($main-bg-color, 3%)};

  --banner-fill: #{lighten($main-theme-color, 10%)};

  --tab-bg: #{$main-bg-color};
  --tab-bg-non-selected: #{darken($main-bg-color, 2%)};
  --tab-bg-active: #{lighten($main-bg-color, 15%)};
  --tab-bg-hover: #{lighten($main-bg-color, 1%)};
  --tab-bg-hover-non-selected: #{darken($main-bg-color, 1%)};

  --toast-anchor-color: #{$anchor-color};

  --length-indicator-color: var(--action-button-fill-color);
}


code:
:root {
  --nav-bg: #{lighten($body-bg-color, 10%)};
  --nav-a-bg-hover: #{lighten($body-bg-color, 25%)};
  --nav-active-bg: #{lighten($body-bg-color, 35%)};
  --nav-a-selected-bg: #{lighten($body-bg-color, 25%)};
  --nav-a-selected-bg-hover: #{lighten($body-bg-color, 40%)};
  --nav-a-selected-active-bg: #{lighten($body-bg-color, 50%)};

  --button-primary-bg: #{darken($main-theme-color, 5%)};
}


code:
$scrollbar-face: #{lighten($main-theme-color, 5%)} !default;
$scrollbar-face-hover: #{lighten($main-theme-color, 7%)} !default;
$scrollbar-face-active: #{lighten($main-theme-color, 3%)} !default;
$scrollbar-track: #{lighten($body-bg-color, 7%)} !default;

@import "_scrollbars.scss";