code:
module.exports = [
  { id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true },
  { id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true },
  { id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true },
  { id: 'fa-globe', src: 'src/thirdparty/font-awesome-svg-png/white/svg/globe.svg' },
  { id: 'fa-gear', src: 'src/thirdparty/font-awesome-svg-png/white/svg/gear.svg', inline: true },
  { id: 'fa-reply', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply.svg' },
  { id: 'fa-reply-all', src: 'src/thirdparty/font-awesome-svg-png/white/svg/reply-all.svg' },
  { id: 'fa-retweet', src: 'src/thirdparty/font-awesome-svg-png/white/svg/retweet.svg' },
  { id: 'fa-star', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star.svg' },
  { id: 'fa-star-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/star-o.svg' },
  { id: 'fa-ellipsis-h', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ellipsis-h.svg' },
  { id: 'fa-spinner', src: 'src/thirdparty/font-awesome-svg-png/white/svg/spinner.svg' },
  { id: 'fa-user', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user.svg' },
  { id: 'fa-play-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/play-circle.svg' },
  { id: 'fa-eye', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye.svg' },
  { id: 'fa-eye-slash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/eye-slash.svg' },
  { id: 'fa-lock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/lock.svg' },
  { id: 'fa-unlock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/unlock.svg' },
  { id: 'fa-envelope', src: 'src/thirdparty/font-awesome-svg-png/white/svg/envelope.svg' },
  { id: 'fa-user-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-times.svg' },
  { id: 'fa-user-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/user-plus.svg' },
  { id: 'fa-external-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/external-link.svg' },
  { id: 'fa-search', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search.svg', inline: true },
  { id: 'fa-comments', src: 'src/thirdparty/font-awesome-svg-png/white/svg/comments.svg', inline: true },
  { id: 'fa-paperclip', src: 'src/thirdparty/font-awesome-svg-png/white/svg/paperclip.svg' },
  { id: 'fa-thumb-tack', src: 'src/thirdparty/font-awesome-svg-png/white/svg/thumb-tack.svg' },
  { id: 'fa-bars', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bars.svg' },
  { id: 'fa-ban', src: 'src/thirdparty/font-awesome-svg-png/white/svg/ban.svg' },
  { id: 'fa-camera', src: 'src/thirdparty/font-awesome-svg-png/white/svg/camera.svg' },
  { id: 'fa-smile', src: 'src/thirdparty/font-awesome-svg-png/white/svg/smile-o.svg' },
  { id: 'fa-exclamation-triangle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/exclamation-triangle.svg' },
  { id: 'fa-check', src: 'src/thirdparty/font-awesome-svg-png/white/svg/check.svg' },
  { id: 'fa-trash', src: 'src/thirdparty/font-awesome-svg-png/white/svg/trash-o.svg' },
  { id: 'fa-hourglass', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hourglass.svg' },
  { id: 'fa-pencil', src: 'src/thirdparty/font-awesome-svg-png/white/svg/pencil.svg' },
  { id: 'fa-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/times.svg' },
  { id: 'fa-volume-off', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-off.svg' },
  { id: 'fa-volume-up', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-up.svg' },
  { id: 'fa-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/link.svg' },
  { id: 'fa-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle.svg' },
  { id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
  { id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
  { id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' },
  { id: 'fa-angle-down', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-down.svg' },
  { id: 'fa-search-minus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-minus.svg' },
  { id: 'fa-search-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-plus.svg' },
  { id: 'fa-share-square-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/share-square-o.svg' },
  { id: 'fa-flag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/flag.svg' },
  { id: 'fa-suitcase', src: 'src/thirdparty/font-awesome-svg-png/white/svg/suitcase.svg' },
  { id: 'fa-bar-chart', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bar-chart.svg' },
  { id: 'fa-clock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/clock-o.svg' },
  { id: 'fa-refresh', src: 'src/thirdparty/font-awesome-svg-png/white/svg/refresh.svg' },
  { id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' }
]


code:
import fetch from 'node-fetch'
import { actions } from './mastodon-data'

const numStatuses = actions
  .map(_ => _.post || _.boost)
  .filter(Boolean)
  .filter(_ => _.privacy !== 'direct')
  .length

async function waitForMastodonData () {
  while (true) {
    try {
      let json = await ((await fetch('http://127.0.0.1:3000/api/v1/instance')).json())
      if (json.stats.status_count === numStatuses) {
        break
      } else {
        console.log('Waiting for number of statuses to equal ' +
          numStatuses + ' (currently ' + json.stats.status_count + ')...')
      }
    } catch (err) {
      console.log('Waiting for Mastodon API to be available...')
    } finally {
      await new Promise(resolve => setTimeout(resolve, 5000))
    }
  }
  console.log('Mastodon data populated')
}

if (require.main === module) {
  waitForMastodonData().catch(err => {
    console.error(err)
    process.exit(1)
  })
}

repo_name: RayoGundead/pinny

license: mit


code:
import fetch from 'node-fetch'

export async function waitForMastodonUiToStart () {
  while (true) {
    try {
      let html = await ((await fetch('http://127.0.0.1:3035/packs/common.js')).text())
      if (html) {
        break
      }
    } catch (err) {
      console.log('Waiting for Mastodon UI to start up...')
      await new Promise(resolve => setTimeout(resolve, 5000))
    }
  }
  console.log('Mastodon UI started up')
}

export async function waitForMastodonApiToStart () {
  while (true) {
    try {
      let json = await ((await fetch('http://127.0.0.1:3000/api/v1/instance')).json())
      if (json.uri) {
        break
      }
    } catch (err) {
      console.log('Waiting for Mastodon API to start up...')
      await new Promise(resolve => setTimeout(resolve, 5000))
    }
  }
  console.log('Mastodon API started up')
}

if (require.main === module) {
  Promise.resolve()
    .then(waitForMastodonApiToStart)
    .then(waitForMastodonUiToStart).catch(err => {
      console.error(err)
      process.exit(1)
    })
}


code:
Admin Guide
====

This guide is for instance admins who may be having trouble with Pinafore compatibility.

By default, [Mastodon allows cross-origin access to the `/api` endpoint](https://github.com/tootsuite/mastodon/blob/50529cbceb84e611bca497624a7a4c38113e5135/config/initializers/cors.rb#L15-L20). Thus Pinafore should "just work" for most Mastodon servers.

If the nginx/Apache settings have been changed, though, then Pinafore might not be able to connect to an instance. To check if the instance is supported, run this command:

```bash
curl -sLv example.com/api/v1/instance -o /dev/null
```

If you see this in the output:

```
Access-Control-Allow-Origin: *
```

Then Pinafore should work as expected!

Otherwise, if the instance admin would like to whitelist only certain websites (including Pinafore) to work with CORS, then they will need to make sure that the server echoes:

```
Access-Control-Allow-Origin: https://pinafore.social
```

when Pinafore tries to access it. Note that this is a bit complicated to configure (compared to the simpler `*` approach), but [there are instructions on StackOverflow](https://stackoverflow.com/q/1653308) for nginx, Apache, and other servers.


code:
## Theming

This document describes how to write your own theme for Pinafore.

First, create a file `scss/themes/foobar.scss`, write some SCSS inside and add
the following at the bottom of `scss/themes/foobar.scss`.
```scss
@import "_base.scss";

body.theme-foobar {
  @include baseTheme();
}
```

> Note: You can find all the SCSS variables available in `scss/themes/_default.scss`
> while the all CSS Custom Properties available are listed in `scss/themes/_base.scss`.

Then, Add your theme to `src/routes/_static/themes.js`
```js
const themes = [
  ...
  {
    name: 'foobar',
    label: 'Foobar', // user-visible name
    color: 'magenta', // main theme color
    dark: true // whether it's a dark theme or not
  }
]
```

Start the development server (`yarn run dev`), go to
`http://localhost:4002/settings/instances/your-instance-name` and select your
newly-created theme. Once you've done that, you can update your theme, and refresh
the page to see the change (you don't have to restart the server).


code:
# Pinafore user guide

This will walk you through the basic usage of Pinafore and its major differences with the Mastodon web UI.

* [Home page](#home-page)
* [Community page](#community-page)
* [Multiple instances](#multiple-instances)

## Home page

The home page is both where you write toots and where you read your home timeline:

![Screenshot of home page](https://github.com/nolanlawson/pinafore/blob/master/docs/Screenshot1.png)

When you scroll down, the "toot" button transforms into a "compose" button:

![Screenshot showing home page scrolled down](https://github.com/nolanlawson/pinafore/blob/master/docs/Screenshot2.png)

If you press this compose button, you can toot anywhere in your home timeline.

![Screenshot showing compose box in a modal dialog](https://github.com/nolanlawson/pinafore/blob/master/docs/Screenshot3.png)

## Community page

By default, the Local Timeline is pinned to the top navigation. In the "Community" section, you can view
Lists, the Local Timeline, the Federated Timeline, and your Favorites, and you can also pin any of these to
the top navigation.

![Screenshot of Community page with options to pin multiple timelines](https://github.com/nolanlawson/pinafore/blob/master/docs/Screenshot4.png)

## Multiple instances

You can manage your instances in the settings:

![Screenshot of instance settings with a list of instances](https://github.com/nolanlawson/pinafore/blob/master/docs/Screenshot5.png)

For each instance, you can choose a custom theme so that it's easy to
remember which instance you're currently using:

![Screenshot of instance configuration with multiple themes available](https://github.com/nolanlawson/pinafore/blob/master/docs/Screenshot6.png)


pinafore

description: Alternative web client for Mastodon

lint: standard && standard --plugin html 'src/routes/**/*.html'

lint-fix: standard --fix && standard --fix --plugin html 'src/routes/**/*.html'

dev: run-s build-template-html build-assets serve-dev

serve-dev: run-p --race build-template-html-watch sapper-dev

sapper-dev: cross-env NODE_ENV=development PORT=4002 sapper dev

before-build: run-s build-template-html build-assets

build: cross-env NODE_ENV=production run-s build-steps

build-steps: run-s before-build sapper-export build-now-json

sapper-build: sapper build

start: node server.js

build-and-start: run-s build start

build-template-html-watch: node -r esm ./bin/build-template-html.js --watch

build-assets: node -r esm ./bin/build-assets.js

run-mastodon: node -r esm ./bin/run-mastodon.js

test: cross-env BROWSER=chrome:headless run-s test-browser

test-browser-suite0: run-p --race run-mastodon build-and-start test-mastodon-suite0

test-mastodon-suite0: run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite0

test-browser-suite1: run-p --race run-mastodon build-and-start test-mastodon-suite1

test-mastodon-suite1: run-s wait-for-mastodon-to-start wait-for-mastodon-data testcafe-suite1

testcafe: run-s testcafe-suite0 testcafe-suite1

testcafe-suite0: cross-env-shell testcafe --hostname localhost --skip-js-errors -c 4 $BROWSER tests/spec/0*

testcafe-suite1: cross-env-shell testcafe --hostname localhost --skip-js-errors $BROWSER tests/spec/1*

test-unit: mocha -r esm -r bin/browser-shim.js tests/unit/

wait-for-mastodon-to-start: node -r esm bin/wait-for-mastodon-to-start.js

wait-for-mastodon-data: node -r esm bin/wait-for-mastodon-data.js

deploy-prod: DEPLOY_TYPE=prod ./bin/deploy.sh

deploy-dev: DEPLOY_TYPE=dev ./bin/deploy.sh

sapper-export: cross-env PORT=22939 sapper export

print-export-info: node ./bin/print-export-info.js

export-steps: run-s before-build sapper-export print-export-info

export: cross-env NODE_ENV=production run-s export-steps

now-build: run-s export

build-now-json: node -r esm ./bin/build-now-json.js

globals: fetch IDBKeyRange IDBObjectStore indexedDB requestAnimationFrame requestIdleCallback location localStorage IntersectionObserver URL Event history performance self caches __routes__ __shell__ __assets__ test fixture Element FormData atob btoa Blob Element Image NotificationEvent NodeList DOMParser CSS customElements AbortController

mode: auto

cjs: vars

ignore: sapper svelte

type: git

author: Nolan Lawson <nolan@nolanlawson.com>


code:
#!/usr/bin/env node

const path = require('path')
const express = require('express')
const compression = require('compression')
const { routes: nowRoutes } = require('./now.json')

const { PORT = 4002 } = process.env
const app = express()
const exportDir = path.resolve(__dirname, '__sapper__/export')

const routes = nowRoutes.map(({ src, headers, dest }) => ({
  regex: new RegExp(src),
  headers,
  dest
}))

app.use(compression({ threshold: 0 }))

app.use(express.static(exportDir, {
  setHeaders (res, thisPath) {
    let localPath = '/' + path.relative(exportDir, thisPath)
    for (let { regex, headers } of routes) {
      if (regex.test(localPath)) {
        res.set(headers)
        return
      }
    }
  }
}))

routes.forEach(({ regex, headers, dest }) => {
  app.get(regex, (req, res) => {
    res.set(headers)
    res.sendFile(path.resolve(exportDir, dest ? req.path.replace(regex, dest) : req.path))
  })
})

app.listen(PORT, () => console.log(`listening on port ${PORT}`))

// Handle SIGINT (source: https://git.io/vhJgF)
process.on('SIGINT', () => process.exit(0))


code:
<!doctype html>

  <meta charset='utf-8' >
  <meta name="viewport" content="width=device-width, viewport-fit=cover">
  <meta id='theThemeColor' name='theme-color' content='#4169e1' >
  <meta name="description" content="An alternative web client for Mastodon, focused on speed and simplicity." >

  %sapper.base%

  <link id='theManifest' rel='manifest' href='/manifest.json' >
  <link id='theFavicon' rel='icon' type='image/png' href='/favicon.png' >
  <link rel="apple-touch-icon" href="/apple-icon.png" >
  <meta name="mobile-web-app-capable" content="yes" >
  <meta name="apple-mobile-web-app-title" content="Pinafore" >
  <meta name="apple-mobile-web-app-status-bar-style" content="white" >


code:
import * as sapper from '../__sapper__/client.js'
import { loadPolyfills } from './routes/_utils/loadPolyfills'
import './routes/_utils/serviceWorkerClient'
import './routes/_utils/historyEvents'
import './routes/_utils/loadingMask'

loadPolyfills().then(() => {
  console.log('init()')
  sapper.start({ target: document.querySelector('#sapper') })
})

console.log('process.env.NODE_ENV', process.env.NODE_ENV)

if (module.hot) {
  module.hot.accept()
}


code:
// For perf reasons, this script is run inline to quickly set certain styles.
// To allow CSP to work correctly, we also calculate a sha256 hash during
// the build process and write it to checksum.js.

import { testHasLocalStorageOnce } from '../routes/_utils/testStorage'
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
import { basename } from '../routes/_api/utils'
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'

window.__themeColors = process.env.THEME_COLORS

const safeParse = str => (typeof str === 'undefined' || str === 'undefined') ? undefined : JSON.parse(str)
const hasLocalStorage = testHasLocalStorageOnce()
const currentInstance = hasLocalStorage && safeParse(localStorage.store_currentInstance)

if (currentInstance) {
  // Do prefetch if we're logged in, so we can connect faster to the other origin.
  // Note that /api/v1/instance is basically the only URL that doesn't require credentials,
  // which is why we can do this. Also we do end up calling this on loading the home page,
  // so it's not a wasted request.
  let link = document.createElement('link')
  link.setAttribute('rel', 'prefetch')
  link.setAttribute('href', `${basename(currentInstance)}/api/v1/instance`)
  link.setAttribute('crossorigin', 'anonymous')
  document.head.appendChild(link)
}

let theme = (currentInstance &&
  localStorage.store_instanceThemes &&
  safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]) ||
  DEFAULT_THEME
if (theme !== INLINE_THEME) {
  // switch theme ASAP to minimize flash of default theme
  switchToTheme(theme)
}

if (!hasLocalStorage || !currentInstance) {
  // if not logged in, show all these 'hidden-from-ssr' elements
  onUserIsLoggedOut()
}

if (hasLocalStorage && localStorage.store_disableCustomScrollbars === 'true') {
  // if user has disabled custom scrollbars, remove this style
  let theScrollbarStyle = document.getElementById('theScrollbarStyle')
  theScrollbarStyle.setAttribute('media', 'only x') // disables the style
}

// hack to make the scrollbars rounded only on macOS
if (/mac/i.test(navigator.platform)) {
  document.documentElement.style.setProperty('--scrollbar-border-radius', '50px')
}

// Versions of iOS Safari before iOS 12.2 do not work properly as a PWA
// for cross-origin authentication: https://github.com/nolanlawson/pinafore/issues/45
// Here we sniff for iOS <12.2 by checking for the existence of a native IntersectionObserver
// function, which was added in 12.2.
if (/iP(?:hone|ad|od)/.test(navigator.userAgent) &&
  !(typeof IntersectionObserver === 'function' &&
    IntersectionObserver.toString().includes('[native code]'))) {
  document.head.removeChild(document.getElementById('theManifest'))
}


code:
import { getAccountAccessibleName } from './getAccountAccessibleName'
import { POST_PRIVACY_OPTIONS } from '../_static/statuses'

function getNotificationText (notification, omitEmojiInDisplayNames) {
  if (!notification) {
    return
  }
  let notificationAccountDisplayName = getAccountAccessibleName(notification.account, omitEmojiInDisplayNames)
  if (notification.type === 'reblog') {
    return `${notificationAccountDisplayName} boosted your status`
  } else if (notification.type === 'favourite') {
    return `${notificationAccountDisplayName} favorited your status`
  }
}

function getPrivacyText (visibility) {
  for (let option of POST_PRIVACY_OPTIONS) {
    if (option.key === visibility) {
      return option.label
    }
  }
}

function getReblogText (reblog, account, omitEmojiInDisplayNames) {
  if (!reblog) {
    return
  }
  let accountDisplayName = getAccountAccessibleName(account, omitEmojiInDisplayNames)
  return `Boosted by ${accountDisplayName}`
}

function cleanupText (text) {
  return text.replace(/\s+/g, ' ').trim()
}

export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
  timeagoFormattedDate, spoilerText, showContent,
  reblog, notification, visibility, omitEmojiInDisplayNames,
  disableLongAriaLabels) {
  let originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
  let contentTextToShow = (showContent || !spoilerText)
    ? cleanupText(plainTextContent)
    : `Content warning: ${cleanupText(spoilerText)}`
  let privacyText = getPrivacyText(visibility)

  if (disableLongAriaLabels) {
    // Long text can crash NVDA; allow users to shorten it like we had it before.
    // https://github.com/nolanlawson/pinafore/issues/694
    return `${privacyText} status by ${originalAccountDisplayName}`
  }

  let values = [
    getNotificationText(notification, omitEmojiInDisplayNames),
    originalAccountDisplayName,
    contentTextToShow,
    timeagoFormattedDate,
    `@${originalAccount.acct}`,
    privacyText,
    getReblogText(reblog, account, omitEmojiInDisplayNames)
  ].filter(Boolean)

  return values.join(', ')
}


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

export function getAccountAccessibleName (account, omitEmojiInDisplayNames) {
  let emojis = account.emojis
  let displayName = account.display_name || account.username
  if (omitEmojiInDisplayNames) {
    displayName = removeEmoji(displayName, emojis) || displayName
  }
  return displayName
}


code:
import { getAccount } from '../_api/user'
import { getRelationship } from '../_api/relationships'
import { database } from '../_database/database'
import { store } from '../_store/store'

async function _updateAccount (accountId, instanceName, accessToken) {
  let localPromise = database.getAccount(instanceName, accountId)
  let remotePromise = getAccount(instanceName, accessToken, accountId).then(account => {
    /* no await */ database.setAccount(instanceName, account)
    return account
  })

  try {
    store.set({ currentAccountProfile: (await localPromise) })
  } catch (e) {
    console.error(e)
  }
  try {
    store.set({ currentAccountProfile: (await remotePromise) })
  } catch (e) {
    console.error(e)
  }
}

async function _updateRelationship (accountId, instanceName, accessToken) {
  let localPromise = database.getRelationship(instanceName, accountId)
  let remotePromise = getRelationship(instanceName, accessToken, accountId).then(relationship => {
    /* no await */ database.setRelationship(instanceName, relationship)
    return relationship
  })
  try {
    store.set({ currentAccountRelationship: (await localPromise) })
  } catch (e) {
    console.error(e)
  }
  try {
    store.set({ currentAccountRelationship: (await remotePromise) })
  } catch (e) {
    console.error(e)
  }
}

export async function updateLocalRelationship (instanceName, accountId, relationship) {
  await database.setRelationship(instanceName, relationship)
  try {
    store.set({ currentAccountRelationship: relationship })
  } catch (e) {
    console.error(e)
  }
}

export async function clearProfileAndRelationship () {
  store.set({
    currentAccountProfile: null,
    currentAccountRelationship: null
  })
}

export async function updateProfileAndRelationship (accountId) {
  let { currentInstance, accessToken } = store.get()

  await Promise.all([
    _updateAccount(accountId, currentInstance, accessToken),
    _updateRelationship(accountId, currentInstance, accessToken)
  ])
}

export async function updateRelationship (accountId) {
  let { currentInstance, accessToken } = store.get()

  await _updateRelationship(accountId, currentInstance, accessToken)
}


code:
import { getAccessTokenFromAuthCode, registerApplication, generateAuthLink } from '../_api/oauth'
import { getInstanceInfo } from '../_api/instance'
import { goto } from '../../../__sapper__/client'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
import { store } from '../_store/store'
import { updateVerifyCredentialsForInstance } from './instances'
import { updateCustomEmojiForInstance } from './emoji'
import { database } from '../_database/database'

const REDIRECT_URI = (typeof location !== 'undefined'
  ? location.origin : 'https://pinafore.social') + '/settings/instances/add'

async function redirectToOauth () {
  let { instanceNameInSearch, loggedInInstances } = store.get()
  instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/$/, '').replace('/$', '').toLowerCase()
  if (Object.keys(loggedInInstances).includes(instanceNameInSearch)) {
    let err = new Error(`You've already logged in to ${instanceNameInSearch}`)
    err.knownError = true
    throw err
  }
  let registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI)
  let instanceInfo = await getInstanceInfo(instanceNameInSearch)
  await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
  let instanceData = await registrationPromise
  store.set({
    currentRegisteredInstanceName: instanceNameInSearch,
    currentRegisteredInstance: instanceData
  })
  store.save()
  let oauthUrl = generateAuthLink(
    instanceNameInSearch,
    instanceData.client_id,
    REDIRECT_URI
  )
  document.location.href = oauthUrl
}

export async function logInToInstance () {
  store.set({
    logInToInstanceLoading: true,
    logInToInstanceError: null
  })
  try {
    await redirectToOauth()
  } catch (err) {
    console.error(err)
    let error = `${err.message || err.name}. ` +
      (err.knownError ? '' : (navigator.onLine
        ? `Is this a valid Mastodon instance? Is a browser extension
           blocking the request? Are you in private browsing mode?`
        : `Are you offline?`))
    let { instanceNameInSearch } = store.get()
    store.set({
      logInToInstanceError: error,
      logInToInstanceErrorForText: instanceNameInSearch
    })
  } finally {
    store.set({ logInToInstanceLoading: false })
  }
}

async function registerNewInstance (code) {
  let { currentRegisteredInstanceName, currentRegisteredInstance } = store.get()
  let instanceData = await getAccessTokenFromAuthCode(
    currentRegisteredInstanceName,
    currentRegisteredInstance.client_id,
    currentRegisteredInstance.client_secret,
    code,
    REDIRECT_URI
  )
  let { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
  instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME
  loggedInInstances[currentRegisteredInstanceName] = instanceData
  if (!loggedInInstancesInOrder.includes(currentRegisteredInstanceName)) {
    loggedInInstancesInOrder.push(currentRegisteredInstanceName)
  }
  store.set({
    instanceNameInSearch: '',
    currentRegisteredInstanceName: null,
    currentRegisteredInstance: null,
    loggedInInstances: loggedInInstances,
    currentInstance: currentRegisteredInstanceName,
    loggedInInstancesInOrder: loggedInInstancesInOrder,
    instanceThemes: instanceThemes
  })
  store.save()
  switchToTheme(DEFAULT_THEME)
  // fire off these requests so they're cached
  /* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
  /* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
  goto('/')
}

export async function handleOauthCode (code) {
  try {
    store.set({ logInToInstanceLoading: true })
    await registerNewInstance(code)
  } catch (err) {
    store.set({ logInToInstanceError: `${err.message || err.name}. Failed to connect to instance.` })
  } finally {
    store.set({ logInToInstanceLoading: false })
  }
}


code:
import { mark, stop } from '../_utils/marks'
import { store } from '../_store/store'
import uniqBy from 'lodash-es/uniqBy'
import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database'
import { concat } from '../_utils/arrays'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { timelineItemToSummary } from '../_utils/timelineItemToSummary'

function getExistingItemIdsSet (instanceName, timelineName) {
  let timelineItemSummaries = store.getForTimeline(instanceName, timelineName, 'timelineItemSummaries') || []
  return new Set(timelineItemSummaries.map(_ => _.id))
}

function removeDuplicates (instanceName, timelineName, updates) {
  // remove duplicates, including duplicates due to reblogs
  let existingItemIds = getExistingItemIdsSet(instanceName, timelineName)
  return updates.filter(update => !existingItemIds.has(update.id))
}

async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) {
  updates = removeDuplicates(instanceName, timelineName, updates)

  if (!updates.length) {
    return
  }

  await database.insertTimelineItems(instanceName, timelineName, updates)

  let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd') || []
  console.log('itemSummariesToAdd', JSON.parse(JSON.stringify(itemSummariesToAdd)))
  console.log('updates.map(timelineItemToSummary)', JSON.parse(JSON.stringify(updates.map(timelineItemToSummary))))
  console.log('concat(itemSummariesToAdd, updates.map(timelineItemToSummary))',
    JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(timelineItemToSummary)))))
  let newItemSummariesToAdd = uniqBy(
    concat(itemSummariesToAdd, updates.map(timelineItemToSummary)),
    _ => _.id
  )
  if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) {
    console.log('adding ', (newItemSummariesToAdd.length - itemSummariesToAdd.length),
      'items to timelineItemSummariesToAdd for timeline', timelineName)
    store.setForTimeline(instanceName, timelineName, { timelineItemSummariesToAdd: newItemSummariesToAdd })
  }
}

function isValidStatusForThread (thread, timelineName, itemSummariesToAdd) {
  let itemSummariesToAddIdSet = new Set(itemSummariesToAdd.map(_ => _.id))
  let threadIdSet = new Set(thread.map(_ => _.id))
  let focusedStatusId = timelineName.split('/')[1] // e.g. "status/123456"
  let focusedStatusIdx = thread.findIndex(_ => _.id === focusedStatusId)
  return status => {
    let repliedToStatusIdx = thread.findIndex(_ => _.id === status.in_reply_to_id)
    return (
      // A reply to an ancestor status is not valid for this thread, but for the focused status
      // itself or any of its descendents, it is valid.
      repliedToStatusIdx >= focusedStatusIdx &&
      // Not a duplicate
      !threadIdSet.has(status.id) &&
      // Not already about to be added
      !itemSummariesToAddIdSet.has(status.id)
    )
  }
}

async function insertUpdatesIntoThreads (instanceName, updates) {
  if (!updates.length) {
    return
  }

  let threads = store.getThreads(instanceName)
  let timelineNames = Object.keys(threads)
  for (let timelineName of timelineNames) {
    let thread = threads[timelineName]

    let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd') || []
    let validUpdates = updates.filter(isValidStatusForThread(thread, timelineName, itemSummariesToAdd))
    if (!validUpdates.length) {
      continue
    }
    let newItemSummariesToAdd = uniqBy(
      concat(itemSummariesToAdd, validUpdates.map(timelineItemToSummary)),
      _ => _.id
    )
    if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) {
      console.log('adding ', (newItemSummariesToAdd.length - itemSummariesToAdd.length),
        'items to timelineItemSummariesToAdd for thread', timelineName)
      store.setForTimeline(instanceName, timelineName, { timelineItemSummariesToAdd: newItemSummariesToAdd })
    }
  }
}

async function processFreshUpdates (instanceName, timelineName) {
  mark('processFreshUpdates')
  let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates')
  if (freshUpdates && freshUpdates.length) {
    let updates = freshUpdates.slice()
    store.setForTimeline(instanceName, timelineName, { freshUpdates: [] })

    await Promise.all([
      insertUpdatesIntoTimeline(instanceName, timelineName, updates),
      insertUpdatesIntoThreads(instanceName, updates.filter(status => status.in_reply_to_id))
    ])
  }
  stop('processFreshUpdates')
}

function lazilyProcessFreshUpdates (instanceName, timelineName) {
  scheduleIdleTask(() => {
    /* no await */ processFreshUpdates(instanceName, timelineName)
  })
}

export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
  addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])
}

export function addStatusesOrNotifications (instanceName, timelineName, newStatusesOrNotifications) {
  console.log('addStatusesOrNotifications', Date.now())
  let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') || []
  freshUpdates = concat(freshUpdates, newStatusesOrNotifications)
  freshUpdates = uniqBy(freshUpdates, _ => _.id)
  store.setForTimeline(instanceName, timelineName, { freshUpdates: freshUpdates })
  lazilyProcessFreshUpdates(instanceName, timelineName)
}


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

export async function insertUsername (realm, username, startIndex, endIndex) {
  let { currentInstance } = store.get()
  let oldText = store.getComposeData(realm, 'text')
  let pre = oldText.substring(0, startIndex)
  let post = oldText.substring(endIndex)
  let newText = `${pre}@${username} ${post}`
  store.setComposeData(realm, { text: newText })
  store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
}

export async function clickSelectedAutosuggestionUsername (realm) {
  let {
    composeSelectionStart,
    autosuggestSearchText,
    autosuggestSelected,
    autosuggestSearchResults
  } = store.get()
  let account = autosuggestSearchResults[autosuggestSelected]
  let startIndex = composeSelectionStart - autosuggestSearchText.length
  let endIndex = composeSelectionStart
  await insertUsername(realm, account.acct, startIndex, endIndex)
}

export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
  let { currentInstance } = store.get()
  let oldText = store.getComposeData(realm, 'text') || ''
  let pre = oldText.substring(0, startIndex)
  let post = oldText.substring(endIndex)
  let newText = `${pre}:${emoji.shortcode}: ${post}`
  store.setComposeData(realm, { text: newText })
  store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
}

export async function clickSelectedAutosuggestionEmoji (realm) {
  let {
    composeSelectionStart,
    autosuggestSearchText,
    autosuggestSelected,
    autosuggestSearchResults
  } = store.get()
  let emoji = autosuggestSearchResults[autosuggestSelected]
  let startIndex = composeSelectionStart - autosuggestSearchText.length
  let endIndex = composeSelectionStart
  await insertEmojiAtPosition(realm, emoji, startIndex, endIndex)
}

export function selectAutosuggestItem (item) {
  let {
    currentComposeRealm,
    composeSelectionStart,
    autosuggestSearchText
  } = store.get()
  let startIndex = composeSelectionStart - autosuggestSearchText.length
  let endIndex = composeSelectionStart
  if (item.acct) {
    /* no await */ insertUsername(currentComposeRealm, item.acct, startIndex, endIndex)
  } else {
    /* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex)
  }
}


code:
import { database } from '../_database/database'
import { store } from '../_store/store'
import { search } from '../_api/search'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { concat } from '../_utils/arrays'
import uniqBy from 'lodash-es/uniqBy'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { PromiseThrottler } from '../_utils/PromiseThrottler'

const DATABASE_SEARCH_RESULTS_LIMIT = 30
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms

function byUsername (a, b) {
  let usernameA = a.acct.toLowerCase()
  let usernameB = b.acct.toLowerCase()

  return usernameA < usernameB ? -1 : usernameA === usernameB ? 0 : 1
}

function byAccountId (a) {
  return a.id
}

export function doAccountSearch (searchText) {
  let canceled = false
  let localResults
  let remoteResults
  let { currentInstance, accessToken } = store.get()
  let controller = typeof AbortController === 'function' && new AbortController()

  function abortFetch () {
    if (controller) {
      controller.abort()
      controller = null
    }
  }

  async function searchAccountsLocally (searchText) {
    localResults = await database.searchAccountsByUsername(
      currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
  }

  async function searchAccountsRemotely (searchText) {
    // Throttle our XHRs to be a good citizen and not spam the server with one XHR per keystroke
    await promiseThrottler.next()
    if (canceled) {
      return
    }
    remoteResults = (await search(
      currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, controller && controller.signal
    )).accounts
  }

  function mergeAndTruncateResults () {
    // Always include local results; they are more likely to be relevant
    // because the user has seen their content before. Otherwise, sort by username.
    let results = (localResults || [])
      .slice()
      .sort(byUsername)
      .slice(0, SEARCH_RESULTS_LIMIT)

    if (results.length < SEARCH_RESULTS_LIMIT) {
      let topRemoteResults = (remoteResults || [])
        .sort(byUsername)
        .slice(0, SEARCH_RESULTS_LIMIT - results.length)
      results = concat(results, topRemoteResults)
      results = uniqBy(results, byAccountId)
    }

    return results
  }

  function onNewResults () {
    if (canceled) {
      return
    }
    let results = mergeAndTruncateResults()
    store.setForCurrentAutosuggest({
      autosuggestType: 'account',
      autosuggestSelected: 0,
      autosuggestSearchResults: results
    })
  }

  scheduleIdleTask(() => {
    if (canceled) {
      return
    }
    // run the two searches in parallel
    searchAccountsLocally(searchText).then(onNewResults).catch(err => {
      console.error('could not search locally', err)
    })
    searchAccountsRemotely(searchText).then(onNewResults).catch(err => {
      console.error('could not search remotely', err)
    })
  })

  return {
    cancel: () => {
      canceled = true
      abortFetch()
    }
  }
}


code:
import { store } from '../_store/store'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'

function searchEmoji (searchText) {
  searchText = searchText.toLowerCase().substring(1)
  let { currentCustomEmoji } = store.get()
  let results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText))
    .sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1)
    .slice(0, SEARCH_RESULTS_LIMIT)
  return results
}

export function doEmojiSearch (searchText) {
  let canceled = false

  scheduleIdleTask(() => {
    if (canceled) {
      return
    }
    let results = searchEmoji(searchText)
    store.setForCurrentAutosuggest({
      autosuggestType: 'emoji',
      autosuggestSelected: 0,
      autosuggestSearchResults: results
    })
  })

  return {
    cancel: () => {
      canceled = true
    }
  }
}


code:
import { store } from '../_store/store'
import { blockAccount, unblockAccount } from '../_api/block'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus'

export async function setAccountBlocked (accountId, block, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    let relationship
    if (block) {
      relationship = await blockAccount(currentInstance, accessToken, accountId)
    } else {
      relationship = await unblockAccount(currentInstance, accessToken, accountId)
    }
    await updateLocalRelationship(currentInstance, accountId, relationship)
    if (toastOnSuccess) {
      if (block) {
        toast.say('Blocked account')
      } else {
        toast.say('Unblocked account')
      }
    }
    emit('refreshAccountsList')
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${block ? 'block' : 'unblock'} account: ` + (e.message || ''))
  }
}


code:
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { postStatus as postStatusToServer } from '../_api/statuses'
import { addStatusOrNotification } from './addStatusOrNotification'
import { database } from '../_database/database'
import { emit } from '../_utils/eventBus'
import { putMediaDescription } from '../_api/media'

export async function insertHandleForReply (statusId) {
  let { currentInstance } = store.get()
  let status = await database.getStatus(currentInstance, statusId)
  let { currentVerifyCredentials } = store.get()
  let originalStatus = status.reblog || status
  let accounts = [originalStatus.account].concat(originalStatus.mentions || [])
    .filter(account => account.id !== currentVerifyCredentials.id)
  if (!store.getComposeData(statusId, 'text') && accounts.length) {
    store.setComposeData(statusId, {
      text: accounts.map(account => `@${account.acct} `).join('')
    })
  }
}

export async function postStatus (realm, text, inReplyToId, mediaIds,
  sensitive, spoilerText, visibility,
  mediaDescriptions, inReplyToUuid, poll) {
  let { currentInstance, accessToken, online } = store.get()

  if (!online) {
    toast.say('You cannot post while offline')
    return
  }

  text = text || ''
  mediaDescriptions = mediaDescriptions || []

  store.set({
    postingStatus: true
  })
  try {
    await Promise.all(mediaDescriptions.map(async (description, i) => {
      return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
    }))
    let status = await postStatusToServer(currentInstance, accessToken, text,
      inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
    addStatusOrNotification(currentInstance, 'home', status)
    store.clearComposeData(realm)
    emit('postedStatus', realm, inReplyToUuid)
  } catch (e) {
    console.error(e)
    toast.say('Unable to post status: ' + (e.message || ''))
  } finally {
    store.set({ postingStatus: false })
  }
}

export function setReplySpoiler (realm, spoiler) {
  let contentWarning = store.getComposeData(realm, 'contentWarning')
  let contentWarningShown = store.getComposeData(realm, 'contentWarningShown')
  if (typeof contentWarningShown !== 'undefined' || contentWarning) {
    return // user has already interacted with the CW
  }
  store.setComposeData(realm, {
    contentWarning: spoiler,
    contentWarningShown: true
  })
}

const PRIVACY_LEVEL = {
  'direct': 1,
  'private': 2,
  'unlisted': 3,
  'public': 4
}

export function setReplyVisibility (realm, replyVisibility) {
  // return the most private between the user's preferred default privacy
  // and the privacy of the status they're replying to
  let postPrivacy = store.getComposeData(realm, 'postPrivacy')
  if (typeof postPrivacy !== 'undefined') {
    return // user has already set the postPrivacy
  }
  let { currentVerifyCredentials } = store.get()
  let defaultVisibility = currentVerifyCredentials.source.privacy
  let visibility = PRIVACY_LEVEL[replyVisibility] < PRIVACY_LEVEL[defaultVisibility]
    ? replyVisibility
    : defaultVisibility
  store.setComposeData(realm, { postPrivacy: visibility })
}


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

export function enablePoll (realm) {
  store.setComposeData(realm, {
    poll: {
      options: [
        '',
        ''
      ]
    }
  })
}

export function disablePoll (realm) {
  store.setComposeData(realm, {
    poll: null
  })
}


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

export function toggleContentWarningShown (realm) {
  let shown = store.getComposeData(realm, 'contentWarningShown')
  let contentWarning = store.getComposeData(realm, 'contentWarning')
  let newShown = !shown
  store.setComposeData(realm, {
    contentWarning: newShown ? contentWarning : '',
    contentWarningShown: newShown
  })
}


code:
import { importShowCopyDialog } from '../_components/dialog/asyncDialogs'
import { toast } from '../_components/toast/toast'

export async function copyText (text) {
  if (navigator.clipboard) { // not supported in all browsers
    try {
      await navigator.clipboard.writeText(text)
      toast.say('Copied to clipboard')
      return
    } catch (e) {
      console.error(e)
    }
  }

  let showCopyDialog = await importShowCopyDialog()
  showCopyDialog(text)
}


code:
import { database } from '../_database/database'

async function getNotification (instanceName, timelineType, timelineValue, itemId) {
  return {
    timelineType,
    timelineValue,
    notification: await database.getNotification(instanceName, itemId)
  }
}

async function getStatus (instanceName, timelineType, timelineValue, itemId) {
  return {
    timelineType,
    timelineValue,
    status: await database.getStatus(instanceName, itemId)
  }
}

export function createMakeProps (instanceName, timelineType, timelineValue) {
  let taskCount = 0
  let pending = []

  // The worker-powered indexeddb promises can resolve in arbitrary order,
  // causing the timeline to load in a jerky way. With this function, we
  // wait for all promises to resolve before resolving them all in one go.
  function awaitAllTasksComplete () {
    return new Promise(resolve => {
      taskCount--
      pending.push(resolve)
      if (taskCount === 0) {
        pending.forEach(_ => _())
        pending = []
      }
    })
  }

  return (itemId) => {
    taskCount++
    let promise = timelineType === 'notifications'
      ? getNotification(instanceName, timelineType, timelineValue, itemId)
      : getStatus(instanceName, timelineType, timelineValue, itemId)

    return promise.then(res => {
      return awaitAllTasksComplete().then(() => res)
    })
  }
}


code:
import { store } from '../_store/store'
import { deleteStatus } from '../_api/delete'
import { toast } from '../_components/toast/toast'
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'

export async function doDeleteStatus (statusId) {
  let { currentInstance, accessToken } = store.get()
  try {
    await deleteStatus(currentInstance, accessToken, statusId)
    deleteStatusLocally(currentInstance, statusId)
    toast.say('Status deleted.')
  } catch (e) {
    console.error(e)
    toast.say('Unable to delete status: ' + (e.message || ''))
  }
}


code:
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs'
import { doDeleteStatus } from './delete'
import { store } from '../_store/store'

export async function deleteAndRedraft (status) {
  let deleteStatusPromise = doDeleteStatus(status.id)
  let dialogPromise = importShowComposeDialog()
  await deleteStatusPromise

  store.setComposeData('dialog', {
    text: statusHtmlToPlainText(status.content, status.mentions),
    contentWarningShown: !!status.spoiler_text,
    contentWarning: status.spoiler_text || '',
    postPrivacy: status.visibility,
    media: status.media_attachments && status.media_attachments.map(_ => ({
      description: _.description || '',
      data: _
    })),
    inReplyToId: status.in_reply_to_id
  })
  let showComposeDialog = await dialogPromise
  showComposeDialog()
}


code:
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
import { store } from '../_store/store'
import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'

function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
  let keys = ['timelineItemSummaries', 'timelineItemSummariesToAdd']
  let summaryFilter = _ => idFilter(_.id)

  keys.forEach(key => {
    let timelineData = store.getAllTimelineData(instanceName, key)
    Object.keys(timelineData).forEach(timelineName => {
      let summaries = timelineData[timelineName]
      if (!timelineFilter(timelineName)) {
        return
      }
      let filteredSummaries = summaries.filter(summaryFilter)
      if (!isEqual(summaries, filteredSummaries)) {
        console.log('deleting an item from timelineName', timelineName, 'for key', key)
        store.setForTimeline(instanceName, timelineName, {
          [key]: filteredSummaries
        })
      }
    })
  })
}

function deleteStatusIdsFromStore (instanceName, idsToDelete) {
  let idsToDeleteSet = new Set(idsToDelete)
  let idWasNotDeleted = id => !idsToDeleteSet.has(id)
  let notNotificationTimeline = timelineName => timelineName !== 'notifications'

  filterItemIdsFromTimelines(instanceName, notNotificationTimeline, idWasNotDeleted)
}

function deleteNotificationIdsFromStore (instanceName, idsToDelete) {
  let idsToDeleteSet = new Set(idsToDelete)
  let idWasNotDeleted = id => !idsToDeleteSet.has(id)
  let isNotificationTimeline = timelineName => timelineName === 'notifications'

  filterItemIdsFromTimelines(instanceName, isNotificationTimeline, idWasNotDeleted)
}

async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
  deleteStatusIdsFromStore(instanceName, statusIdsToDelete)
  deleteNotificationIdsFromStore(instanceName, notificationIdsToDelete)
  await database.deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
}

async function doDeleteStatus (instanceName, statusId) {
  console.log('deleting statusId', statusId)
  let rebloggedIds = await getIdsThatRebloggedThisStatus(instanceName, statusId)
  let statusIdsToDelete = Array.from(new Set([statusId].concat(rebloggedIds).filter(Boolean)))
  let notificationIdsToDelete = Array.from(new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete)))
  await deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
}

export function deleteStatus (instanceName, statusId) {
  scheduleIdleTask(() => {
    /* no await */ doDeleteStatus(instanceName, statusId)
  })
}


code:
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { database } from '../_database/database'
import { getCustomEmoji } from '../_api/emoji'
import { store } from '../_store/store'

export async function updateCustomEmojiForInstance (instanceName) {
  await cacheFirstUpdateAfter(
    () => getCustomEmoji(instanceName),
    () => database.getCustomEmoji(instanceName),
    emoji => database.setCustomEmoji(instanceName, emoji),
    emoji => {
      let { customEmoji } = store.get()
      customEmoji[instanceName] = emoji
      store.set({ customEmoji: customEmoji })
    }
  )
}

export function insertEmoji (realm, emoji) {
  let emojiText = emoji.custom ? emoji.colons : emoji.native
  let { composeSelectionStart } = store.get()
  let idx = composeSelectionStart || 0
  let oldText = store.getComposeData(realm, 'text') || ''
  let pre = oldText.substring(0, idx)
  let post = oldText.substring(idx)
  let newText = `${pre}${emojiText} ${post}`
  store.setComposeData(realm, { text: newText })
}


code:
import { favoriteStatus, unfavoriteStatus } from '../_api/favorite'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { database } from '../_database/database'

export async function setFavorited (statusId, favorited) {
  let { online } = store.get()
  if (!online) {
    toast.say(`You cannot ${favorited ? 'favorite' : 'unfavorite'} while offline.`)
    return
  }
  let { currentInstance, accessToken } = store.get()
  let networkPromise = favorited
    ? favoriteStatus(currentInstance, accessToken, statusId)
    : unfavoriteStatus(currentInstance, accessToken, statusId)
  store.setStatusFavorited(currentInstance, statusId, favorited) // optimistic update
  try {
    await networkPromise
    await database.setStatusFavorited(currentInstance, statusId, favorited)
  } catch (e) {
    console.error(e)
    toast.say(`Failed to ${favorited ? 'favorite' : 'unfavorite'}. ` + (e.message || ''))
    store.setStatusFavorited(currentInstance, statusId, !favorited) // undo optimistic update
  }
}


code:
import { store } from '../_store/store'
import { followAccount, unfollowAccount } from '../_api/follow'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'

export async function setAccountFollowed (accountId, follow, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    let relationship
    if (follow) {
      relationship = await followAccount(currentInstance, accessToken, accountId)
    } else {
      relationship = await unfollowAccount(currentInstance, accessToken, accountId)
    }
    await updateLocalRelationship(currentInstance, accountId, relationship)
    if (toastOnSuccess) {
      if (follow) {
        toast.say('Followed account')
      } else {
        toast.say('Unfollowed account')
      }
    }
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${follow ? 'follow' : 'unfollow'} account: ` + (e.message || ''))
  }
}


code:
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from '../_api/utils'

export async function getFollowRequests (instanceName, accessToken) {
  let url = `${basename(instanceName)}/api/v1/follow_requests`
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}

export async function authorizeFollowRequest (instanceName, accessToken, id) {
  let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/authorize`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function rejectFollowRequest (instanceName, accessToken, id) {
  let url = `${basename(instanceName)}/api/v1/follow_requests/${id}/reject`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { store } from '../_store/store'
import { getTimeline } from '../_api/timelines'

export async function getRecentStatusesForAccount (accountId) {
  const { currentInstance, accessToken } = store.get()
  return getTimeline(currentInstance, accessToken, `account/${accountId}`, null, null, 20)
}


code:
import { getVerifyCredentials } from '../_api/user'
import { store } from '../_store/store'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
import { toast } from '../_components/toast/toast'
import { goto } from '../../../__sapper__/client'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { getInstanceInfo } from '../_api/instance'
import { database } from '../_database/database'

export function changeTheme (instanceName, newTheme) {
  let { instanceThemes } = store.get()
  instanceThemes[instanceName] = newTheme
  store.set({ instanceThemes: instanceThemes })
  store.save()
  let { currentInstance } = store.get()
  if (instanceName === currentInstance) {
    switchToTheme(newTheme)
  }
}

export function switchToInstance (instanceName) {
  let { instanceThemes } = store.get()
  store.set({
    currentInstance: instanceName,
    searchResults: null,
    queryInSearch: ''
  })
  store.save()
  switchToTheme(instanceThemes[instanceName])
}

export async function logOutOfInstance (instanceName) {
  let {
    loggedInInstances,
    instanceThemes,
    loggedInInstancesInOrder,
    composeData,
    currentInstance
  } = store.get()
  loggedInInstancesInOrder.splice(loggedInInstancesInOrder.indexOf(instanceName), 1)
  let newInstance = instanceName === currentInstance
    ? loggedInInstancesInOrder[0]
    : currentInstance
  delete loggedInInstances[instanceName]
  delete instanceThemes[instanceName]
  delete composeData[instanceName]
  store.set({
    loggedInInstances: loggedInInstances,
    instanceThemes: instanceThemes,
    loggedInInstancesInOrder: loggedInInstancesInOrder,
    currentInstance: newInstance,
    searchResults: null,
    queryInSearch: '',
    composeData: composeData
  })
  store.save()
  toast.say(`Logged out of ${instanceName}`)
  switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
  /* no await */ database.clearDatabaseForInstance(instanceName)
  goto('/settings/instances')
}

function setStoreVerifyCredentials (instanceName, thisVerifyCredentials) {
  let { verifyCredentials } = store.get()
  verifyCredentials[instanceName] = thisVerifyCredentials
  store.set({ verifyCredentials: verifyCredentials })
}

export async function updateVerifyCredentialsForInstance (instanceName) {
  let { loggedInInstances } = store.get()
  let accessToken = loggedInInstances[instanceName].access_token
  await cacheFirstUpdateAfter(
    () => getVerifyCredentials(instanceName, accessToken),
    () => database.getInstanceVerifyCredentials(instanceName),
    verifyCredentials => database.setInstanceVerifyCredentials(instanceName, verifyCredentials),
    verifyCredentials => setStoreVerifyCredentials(instanceName, verifyCredentials)
  )
}

export async function updateVerifyCredentialsForCurrentInstance () {
  let { currentInstance } = store.get()
  await updateVerifyCredentialsForInstance(currentInstance)
}

export async function updateInstanceInfo (instanceName) {
  await cacheFirstUpdateAfter(
    () => getInstanceInfo(instanceName),
    () => database.getInstanceInfo(instanceName),
    info => database.setInstanceInfo(instanceName, info),
    info => {
      let { instanceInfos } = store.get()
      instanceInfos[instanceName] = info
      store.set({ instanceInfos: instanceInfos })
    }
  )
}


code:
import { store } from '../_store/store'
import { getLists } from '../_api/lists'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { database } from '../_database/database'

export async function updateListsForInstance (instanceName) {
  let { loggedInInstances } = store.get()
  let accessToken = loggedInInstances[instanceName].access_token

  await cacheFirstUpdateAfter(
    () => getLists(instanceName, accessToken),
    () => database.getLists(instanceName),
    lists => database.setLists(instanceName, lists),
    lists => {
      let { instanceLists } = store.get()
      instanceLists[instanceName] = lists
      store.set({ instanceLists: instanceLists })
    }
  )
}


code:
import { store } from '../_store/store'
import { uploadMedia } from '../_api/media'
import { toast } from '../_components/toast/toast'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'

export async function doMediaUpload (realm, file) {
  let { currentInstance, accessToken } = store.get()
  store.set({ uploadingMedia: true })
  try {
    let response = await uploadMedia(currentInstance, accessToken, file)
    let composeMedia = store.getComposeData(realm, 'media') || []
    if (composeMedia.length === 4) {
      throw new Error('Only 4 media max are allowed')
    }
    composeMedia.push({
      data: response,
      file: { name: file.name },
      description: ''
    })
    store.setComposeData(realm, {
      media: composeMedia
    })
    scheduleIdleTask(() => store.save())
  } catch (e) {
    console.error(e)
    toast.say('Failed to upload media: ' + (e.message || ''))
  } finally {
    store.set({ uploadingMedia: false })
  }
}

export function deleteMedia (realm, i) {
  let composeMedia = store.getComposeData(realm, 'media')
  composeMedia.splice(i, 1)

  store.setComposeData(realm, {
    media: composeMedia
  })
  scheduleIdleTask(() => store.save())
}


code:
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs'
import { store } from '../_store/store'

export async function composeNewStatusMentioning (account) {
  store.setComposeData('dialog', { text: `@${account.acct} ` })
  let showComposeDialog = await importShowComposeDialog()
  showComposeDialog()
}


code:
import { store } from '../_store/store'
import { muteAccount, unmuteAccount } from '../_api/mute'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'
import { emit } from '../_utils/eventBus'

export async function setAccountMuted (accountId, mute, notifications, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    let relationship
    if (mute) {
      relationship = await muteAccount(currentInstance, accessToken, accountId, notifications)
    } else {
      relationship = await unmuteAccount(currentInstance, accessToken, accountId)
    }
    await updateLocalRelationship(currentInstance, accountId, relationship)
    if (toastOnSuccess) {
      if (mute) {
        toast.say('Muted account')
      } else {
        toast.say('Unmuted account')
      }
    }
    emit('refreshAccountsList')
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${mute ? 'mute' : 'unmute'} account: ` + (e.message || ''))
  }
}


code:
import { store } from '../_store/store'
import { muteConversation, unmuteConversation } from '../_api/muteConversation'
import { toast } from '../_components/toast/toast'
import { database } from '../_database/database'

export async function setConversationMuted (statusId, mute, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    if (mute) {
      await muteConversation(currentInstance, accessToken, statusId)
    } else {
      await unmuteConversation(currentInstance, accessToken, statusId)
    }
    await database.setStatusMuted(currentInstance, statusId, mute)
    if (toastOnSuccess) {
      if (mute) {
        toast.say('Muted conversation')
      } else {
        toast.say('Unmuted conversation')
      }
    }
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${mute ? 'mute' : 'unmute'} conversation: ` + (e.message || ''))
  }
}


code:
// When the user is logged out, we need to be sure to re-show all the "hidden from SSR" styles
// so that we don't get a blank page.
export function onUserIsLoggedOut () {
  if (document.getElementById('hiddenFromSsrStyle')) {
    return
  }
  let style = document.createElement('style')
  style.setAttribute('id', 'hiddenFromSsrStyle')
  style.textContent = '.hidden-from-ssr { opacity: 1 !important; }'
  document.head.appendChild(style)
}


code:
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { pinStatus, unpinStatus } from '../_api/pin'
import { database } from '../_database/database'
import { emit } from '../_utils/eventBus'

export async function setStatusPinnedOrUnpinned (statusId, pinned, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    if (pinned) {
      await pinStatus(currentInstance, accessToken, statusId)
    } else {
      await unpinStatus(currentInstance, accessToken, statusId)
    }
    if (toastOnSuccess) {
      if (pinned) {
        toast.say('Pinned status')
      } else {
        toast.say('Unpinned status')
      }
    }
    store.setStatusPinned(currentInstance, statusId, pinned)
    await database.setStatusPinned(currentInstance, statusId, pinned)
    emit('updatePinnedStatuses')
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${pinned ? 'pin' : 'unpin'} status: ` + (e.message || ''))
  }
}


code:
import { store } from '../_store/store'
import { cacheFirstUpdateAfter } from '../_utils/sync'
import { database } from '../_database/database'
import {
  getPinnedStatuses
} from '../_api/pinnedStatuses'

export async function updatePinnedStatusesForAccount (accountId) {
  let { currentInstance, accessToken } = store.get()

  await cacheFirstUpdateAfter(
    () => getPinnedStatuses(currentInstance, accessToken, accountId),
    async () => {
      let pinnedStatuses = await database.getPinnedStatuses(currentInstance, accountId)
      if (!pinnedStatuses || !pinnedStatuses.every(Boolean)) {
        throw new Error('missing pinned statuses in idb')
      }
      return pinnedStatuses
    },
    statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
    statuses => {
      let { pinnedStatuses } = store.get()
      pinnedStatuses[currentInstance] = pinnedStatuses[currentInstance] || {}
      pinnedStatuses[currentInstance][accountId] = statuses
      store.set({ pinnedStatuses: pinnedStatuses })
    }
  )
}


code:
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'

export async function getPoll (pollId) {
  let { currentInstance, accessToken } = store.get()
  try {
    let poll = await getPollApi(currentInstance, accessToken, pollId)
    return poll
  } catch (e) {
    console.error(e)
    toast.say('Unable to refresh poll: ' + (e.message || ''))
  }
}

export async function voteOnPoll (pollId, choices) {
  let { currentInstance, accessToken } = store.get()
  try {
    let poll = await voteOnPollApi(currentInstance, accessToken, pollId, choices.map(_ => _.toString()))
    return poll
  } catch (e) {
    console.error(e)
    toast.say('Unable to vote in poll: ' + (e.message || ''))
  }
}


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

export function setPostPrivacy (realm, postPrivacyKey) {
  store.setComposeData(realm, { postPrivacy: postPrivacyKey })
}


code:
import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription'
import { store } from '../_store/store'
import { urlBase64ToUint8Array } from '../_utils/base64'

const dummyApplicationServerKey = 'BImgAz4cF_yvNFp8uoBJCaGpCX4d0atNIFMHfBvAAXCyrnn9IMAFQ10DW_ZvBCzGeR4fZI5FnEi2JVcRE-L88jY='

export async function updatePushSubscriptionForInstance (instanceName) {
  const { loggedInInstances, pushSubscription } = store.get()
  const accessToken = loggedInInstances[instanceName].access_token

  if (pushSubscription === null) {
    return
  }

  const registration = await navigator.serviceWorker.ready
  const subscription = await registration.pushManager.getSubscription()

  if (subscription === null) {
    store.set({ pushSubscription: null })
    store.save()
    return
  }

  try {
    const backendSubscription = await getSubscription(instanceName, accessToken)

    // Check if applicationServerKey changed (need to get another subscription from the browser)
    if (btoa(urlBase64ToUint8Array(backendSubscription.server_key).buffer) !== btoa(subscription.options.applicationServerKey)) {
      await subscription.unsubscribe()
      await deleteSubscription(instanceName, accessToken)
      await updateAlerts(instanceName, pushSubscription.alerts)
    } else {
      store.set({ pushSubscription: backendSubscription })
      store.save()
    }
  } catch (e) {
    // TODO: Better way to detect 404
    if (e.message.startsWith('404:')) {
      await subscription.unsubscribe()
      store.set({ pushSubscription: null })
      store.save()
    }
  }
}

export async function updateAlerts (instanceName, alerts) {
  const { loggedInInstances } = store.get()
  const accessToken = loggedInInstances[instanceName].access_token

  const registration = await navigator.serviceWorker.ready
  let subscription = await registration.pushManager.getSubscription()

  if (subscription === null) {
    // We need applicationServerKey in order to register a push subscription
    // but the API doesn't expose it as a constant (as it should).
    // So we need to register a subscription with a dummy applicationServerKey,
    // send it to the backend saves it and return applicationServerKey, which
    // we use to register a new subscription.
    // https://github.com/tootsuite/mastodon/issues/8785
    subscription = await registration.pushManager.subscribe({
      applicationServerKey: urlBase64ToUint8Array(dummyApplicationServerKey),
      userVisibleOnly: true
    })

    let backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)

    await subscription.unsubscribe()

    subscription = await registration.pushManager.subscribe({
      applicationServerKey: urlBase64ToUint8Array(backendSubscription.server_key),
      userVisibleOnly: true
    })

    backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)

    store.set({ pushSubscription: backendSubscription })
    store.save()
  } else {
    try {
      const backendSubscription = await putSubscription(instanceName, accessToken, alerts)
      store.set({ pushSubscription: backendSubscription })
      store.save()
    } catch (e) {
      const backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
      store.set({ pushSubscription: backendSubscription })
      store.save()
    }
  }
}


code:
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { reblogStatus, unreblogStatus } from '../_api/reblog'
import { database } from '../_database/database'

export async function setReblogged (statusId, reblogged) {
  let online = store.get()
  if (!online) {
    toast.say(`You cannot ${reblogged ? 'boost' : 'unboost'} while offline.`)
    return
  }
  let { currentInstance, accessToken } = store.get()
  let networkPromise = reblogged
    ? reblogStatus(currentInstance, accessToken, statusId)
    : unreblogStatus(currentInstance, accessToken, statusId)
  store.setStatusReblogged(currentInstance, statusId, reblogged) // optimistic update
  try {
    await networkPromise
    await database.setStatusReblogged(currentInstance, statusId, reblogged)
  } catch (e) {
    console.error(e)
    toast.say(`Failed to ${reblogged ? 'boost' : 'unboost'}. ` + (e.message || ''))
    store.setStatusReblogged(currentInstance, statusId, !reblogged) // undo optimistic update
  }
}


code:
import { importShowReportDialog } from '../_components/dialog/asyncDialogs'

export async function reportStatusOrAccount ({ status, account }) {
  let showReportDialog = await importShowReportDialog()
  showReportDialog({ status, account })
}


code:
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { report } from '../_api/report'

export async function reportStatuses (account, statusIds, comment, forward) {
  let { currentInstance, accessToken } = store.get()
  try {
    await report(currentInstance, accessToken, account.id, statusIds, comment, forward)
    toast.say('Submitted report')
  } catch (e) {
    toast.say('Failed to report: ' + (e.message || ''))
  }
}


code:
import { store } from '../_store/store'
import { approveFollowRequest, rejectFollowRequest } from '../_api/requests'
import { emit } from '../_utils/eventBus'
import { toast } from '../_components/toast/toast'

export async function setFollowRequestApprovedOrRejected (accountId, approved, toastOnSuccess) {
  let {
    currentInstance,
    accessToken
  } = store.get()
  try {
    if (approved) {
      await approveFollowRequest(currentInstance, accessToken, accountId)
    } else {
      await rejectFollowRequest(currentInstance, accessToken, accountId)
    }
    if (toastOnSuccess) {
      if (approved) {
        toast.say('Approved follow request')
      } else {
        toast.say('Rejected follow request')
      }
    }
    emit('refreshAccountsList')
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${approved ? 'approve' : 'reject'} account: ` + (e.message || ''))
  }
}


code:
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
import { search } from '../_api/search'

export async function doSearch () {
  let { currentInstance, accessToken, queryInSearch } = store.get()
  store.set({ searchLoading: true })
  try {
    let results = await search(currentInstance, accessToken, queryInSearch)
    let { queryInSearch: newQueryInSearch } = store.get() // avoid race conditions
    if (newQueryInSearch === queryInSearch) {
      store.set({
        searchResultsForQuery: queryInSearch,
        searchResults: results
      })
    }
  } catch (e) {
    toast.say('Error during search: ' + (e.name || '') + ' ' + (e.message || ''))
    console.error(e)
  } finally {
    store.set({ searchLoading: false })
  }
}


code:
import { store } from '../_store/store'
import { blockDomain, unblockDomain } from '../_api/blockDomain'
import { toast } from '../_components/toast/toast'
import { updateRelationship } from './accounts'

export async function setDomainBlocked (accountId, domain, block, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    if (block) {
      await blockDomain(currentInstance, accessToken, domain)
    } else {
      await unblockDomain(currentInstance, accessToken, domain)
    }
    await updateRelationship(accountId)
    if (toastOnSuccess) {
      if (block) {
        toast.say(`Hiding ${domain}`)
      } else {
        toast.say(`Unhiding ${domain}`)
      }
    }
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${block ? 'hide' : 'unhide'} domain: ` + (e.message || ''))
  }
}


code:
import { store } from '../_store/store'
import { setShowReblogs as setShowReblogsApi } from '../_api/showReblogs'
import { toast } from '../_components/toast/toast'
import { updateLocalRelationship } from './accounts'

export async function setShowReblogs (accountId, showReblogs, toastOnSuccess) {
  let { currentInstance, accessToken } = store.get()
  try {
    let relationship = await setShowReblogsApi(currentInstance, accessToken, accountId, showReblogs)
    await updateLocalRelationship(currentInstance, accountId, relationship)
    if (toastOnSuccess) {
      if (showReblogs) {
        toast.say('Showing boosts')
      } else {
        toast.say('Hiding boosts')
      }
    }
  } catch (e) {
    console.error(e)
    toast.say(`Unable to ${showReblogs ? 'show' : 'hide'} boosts: ` + (e.message || ''))
  }
}


code:
import { toast } from '../_components/toast/toast'
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'

export async function shareStatus (status) {
  try {
    await navigator.share({
      title: status.spoiler_text || undefined,
      text: statusHtmlToPlainText(status.content, status.mentions),
      url: status.url
    })
  } catch (e) {
    toast.say(`Unable to share: ` + (e.message || ''))
  }
}


code:
import { showMoreItemsForCurrentTimeline } from './timeline'
import { scrollToTop } from '../_utils/scrollToTop'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { createStatusOrNotificationUuid } from '../_utils/createStatusOrNotificationUuid'
import { store } from '../_store/store'

const RETRIES = 5
const TIMEOUT = 50

export function showMoreAndScrollToTop () {
  // Similar to Twitter, pressing "." will click the "show more" button and select
  // the first toot.
  showMoreItemsForCurrentTimeline()
  let {
    currentInstance,
    timelineItemSummaries,
    currentTimelineType,
    currentTimelineValue
  } = store.get()
  let firstItemSummary = timelineItemSummaries && timelineItemSummaries[0]
  if (!firstItemSummary) {
    return
  }
  let notificationId = currentTimelineType === 'notifications' && firstItemSummary.id
  let statusId = currentTimelineType !== 'notifications' && firstItemSummary.id
  scrollToTop(/* smooth */ false)
  // try 5 times to wait for the element to be rendered and then focus it
  let count = 0
  const tryToFocusElement = () => {
    let uuid = createStatusOrNotificationUuid(
      currentInstance, currentTimelineType,
      currentTimelineValue, notificationId, statusId
    )
    let element = document.getElementById(uuid)
    if (element) {
      try {
        element.focus({ preventScroll: true })
      } catch (e) {
        console.error(e)
      }
    } else {
      if (++count <= RETRIES) {
        setTimeout(() => scheduleIdleTask(tryToFocusElement), TIMEOUT)
      }
    }
  }
  scheduleIdleTask(tryToFocusElement)
}


code:
import { store } from '../_store/store'
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs'

export async function showShareDialogIfNecessary () {
  let { isUserLoggedIn, openShareDialog } = store.get()
  store.set({ openShareDialog: false })
  if (isUserLoggedIn && openShareDialog) {
    let showComposeDialog = await importShowComposeDialog()
    showComposeDialog()
  }
}


code:
import { database } from '../_database/database'

export async function getIdThatThisStatusReblogged (instanceName, statusId) {
  let status = await database.getStatus(instanceName, statusId)
  return status.reblog && status.reblog.id
}

export async function getIdsThatTheseStatusesReblogged (instanceName, statusIds) {
  let reblogIds = await Promise.all(statusIds.map(async statusId => {
    return getIdThatThisStatusReblogged(instanceName, statusId)
  }))
  return reblogIds.filter(Boolean)
}

export async function getIdsThatRebloggedThisStatus (instanceName, statusId) {
  return database.getReblogsForStatus(instanceName, statusId)
}

export async function getNotificationIdsForStatuses (instanceName, statusIds) {
  return database.getNotificationIdsForStatuses(instanceName, statusIds)
}


code:
import { TimelineStream } from '../_api/TimelineStream'
import { mark, stop } from '../_utils/marks'
import { deleteStatus } from './deleteStatuses'
import { addStatusOrNotification } from './addStatusOrNotification'

function processMessage (instanceName, timelineName, message) {
  mark('processMessage')
  let { event, payload } = message
  if (['update', 'notification', 'conversation'].includes(event)) {
    payload = JSON.parse(payload) // only these payloads are JSON-encoded for some reason
  }

  switch (event) {
    case 'delete':
      deleteStatus(instanceName, payload)
      break
    case 'update':
      addStatusOrNotification(instanceName, timelineName, payload)
      break
    case 'notification':
      addStatusOrNotification(instanceName, 'notifications', payload)
      if (payload.type === 'mention') {
        addStatusOrNotification(instanceName, 'notifications/mentions', payload)
      }
      break
    case 'conversation':
      // This is a hack in order to mostly fit the conversation model into
      // a timeline of statuses. To have a clean implementation we would need to
      // reproduce what is done for statuses for the conversation.
      //
      // It will add new DMs as new conversations instead of updating existing threads
      addStatusOrNotification(instanceName, timelineName, payload.last_status)
      break
  }
  stop('processMessage')
}

export function createStream (streamingApi, instanceName, accessToken,
  timelineName, onOpenStream) {
  return new TimelineStream(streamingApi, accessToken, timelineName, {
    onMessage (msg) {
      if (
        msg.event !== 'update' &&
        msg.event !== 'delete' &&
        msg.event !== 'notification' &&
        msg.event !== 'conversation'
      ) {
        console.error("don't know how to handle event", msg)
        return
      }
      processMessage(instanceName, timelineName, msg)
    },
    onOpen () {
      if (onOpenStream) {
        onOpenStream()
      }
      console.log('opened stream for timeline', timelineName)
    },
    onClose () {
      console.log('closed stream for timeline', timelineName)
    },
    onReconnect () {
      console.log('reconnected stream for timeline', timelineName)
    }
  })
}


code:
import { store } from '../_store/store'
import { getTimeline } from '../_api/timelines'
import { toast } from '../_components/toast/toast'
import { mark, stop } from '../_utils/marks'
import { concat, mergeArrays } from '../_utils/arrays'
import { compareTimelineItemSummaries } from '../_utils/statusIdSorting'
import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database'
import { getStatus, getStatusContext } from '../_api/statuses'
import { emit } from '../_utils/eventBus'
import { TIMELINE_BATCH_SIZE } from '../_static/timelines'
import { timelineItemToSummary } from '../_utils/timelineItemToSummary'

async function storeFreshTimelineItemsInDatabase (instanceName, timelineName, items) {
  await database.insertTimelineItems(instanceName, timelineName, items)
  if (timelineName.startsWith('status/')) {
    // For status threads, we want to be sure to update the favorite/reblog counts even if
    // this is a stale "view" of the status. See 119-status-counts-update.js for
    // an example of why we need this.
    items.forEach(item => {
      emit('statusUpdated', item)
    })
  }
}

async function fetchTimelineItemsFromNetwork (instanceName, accessToken, timelineName, lastTimelineItemId) {
  if (timelineName.startsWith('status/')) { // special case - this is a list of descendents and ancestors
    let statusId = timelineName.split('/').slice(-1)[0]
    let statusRequest = getStatus(instanceName, accessToken, statusId)
    let contextRequest = getStatusContext(instanceName, accessToken, statusId)
    let [ status, context ] = await Promise.all([statusRequest, contextRequest])
    return concat(context.ancestors, status, context.descendants)
  } else { // normal timeline
    return getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, null, TIMELINE_BATCH_SIZE)
  }
}

async function fetchTimelineItems (instanceName, accessToken, timelineName, lastTimelineItemId, online) {
  mark('fetchTimelineItems')
  let items
  let stale = false
  if (!online) {
    items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, TIMELINE_BATCH_SIZE)
    stale = true
  } else {
    try {
      console.log('fetchTimelineItemsFromNetwork')
      items = await fetchTimelineItemsFromNetwork(instanceName, accessToken, timelineName, lastTimelineItemId)
      /* no await */ storeFreshTimelineItemsInDatabase(instanceName, timelineName, items)
    } catch (e) {
      console.error(e)
      toast.say('Internet request failed. Showing offline content.')
      items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, TIMELINE_BATCH_SIZE)
      stale = true
    }
  }
  stop('fetchTimelineItems')
  return { items, stale }
}

async function addTimelineItems (instanceName, timelineName, items, stale) {
  console.log('addTimelineItems, length:', items.length)
  mark('addTimelineItemSummaries')
  let newSummaries = items.map(timelineItemToSummary)
  addTimelineItemSummaries(instanceName, timelineName, newSummaries, stale)
  stop('addTimelineItemSummaries')
}

export async function addTimelineItemSummaries (instanceName, timelineName, newSummaries, newStale) {
  let oldSummaries = store.getForTimeline(instanceName, timelineName, 'timelineItemSummaries') || []
  let oldStale = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesAreStale')

  let mergedSummaries = mergeArrays(oldSummaries, newSummaries, compareTimelineItemSummaries)

  if (!isEqual(oldSummaries, mergedSummaries)) {
    store.setForTimeline(instanceName, timelineName, { timelineItemSummaries: mergedSummaries })
  }
  if (oldStale !== newStale) {
    store.setForTimeline(instanceName, timelineName, { timelineItemSummariesAreStale: newStale })
  }
}

async function fetchTimelineItemsAndPossiblyFallBack () {
  mark('fetchTimelineItemsAndPossiblyFallBack')
  let {
    currentTimeline,
    currentInstance,
    accessToken,
    lastTimelineItemId,
    online
  } = store.get()

  let { items, stale } = await fetchTimelineItems(currentInstance, accessToken, currentTimeline, lastTimelineItemId, online)
  addTimelineItems(currentInstance, currentTimeline, items, stale)
  stop('fetchTimelineItemsAndPossiblyFallBack')
}

export async function setupTimeline () {
  mark('setupTimeline')
  // If we don't have any item summaries, or if the current item summaries are stale
  // (i.e. via offline mode), then we need to re-fetch
  // Also do this if it's a thread, because threads change pretty frequently and
  // we don't have a good way to update them.
  let {
    timelineItemSummaries,
    timelineItemSummariesAreStale,
    currentTimeline
  } = store.get()
  if (!timelineItemSummaries ||
      timelineItemSummariesAreStale ||
      currentTimeline.startsWith('status/')) {
    await fetchTimelineItemsAndPossiblyFallBack()
  }
  stop('setupTimeline')
}

export async function fetchMoreItemsAtBottomOfTimeline (instanceName, timelineName) {
  console.log('setting runningUpdate: true')
  store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
  await fetchTimelineItemsAndPossiblyFallBack()
  console.log('setting runningUpdate: false')
  store.setForTimeline(instanceName, timelineName, { runningUpdate: false })
}

export async function showMoreItemsForTimeline (instanceName, timelineName) {
  mark('showMoreItemsForTimeline')
  let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd') || []
  itemSummariesToAdd = itemSummariesToAdd.sort(compareTimelineItemSummaries).reverse()
  addTimelineItemSummaries(instanceName, timelineName, itemSummariesToAdd, false)
  store.setForTimeline(instanceName, timelineName, {
    timelineItemSummariesToAdd: [],
    shouldShowHeader: false,
    showHeader: false
  })
  stop('showMoreItemsForTimeline')
}

export function showMoreItemsForCurrentTimeline () {
  let { currentInstance, currentTimeline } = store.get()
  return showMoreItemsForTimeline(
    currentInstance,
    currentTimeline
  )
}

export async function showMoreItemsForThread (instanceName, timelineName) {
  mark('showMoreItemsForThread')
  let itemSummariesToAdd = store.getForTimeline(instanceName, timelineName, 'timelineItemSummariesToAdd')
  let timelineItemSummaries = store.getForTimeline(instanceName, timelineName, 'timelineItemSummaries')
  let timelineItemIds = new Set(timelineItemSummaries.map(_ => _.id))
  // TODO: update database and do the thread merge correctly
  for (let itemSummaryToAdd of itemSummariesToAdd) {
    if (!timelineItemIds.has(itemSummaryToAdd.id)) {
      timelineItemSummaries.push(itemSummaryToAdd)
    }
  }
  store.setForTimeline(instanceName, timelineName, {
    timelineItemSummariesToAdd: [],
    timelineItemSummaries: timelineItemSummaries
  })
  stop('showMoreItemsForThread')
}


code:
import { importShowMuteDialog } from '../_components/dialog/asyncDialogs'
import { setAccountMuted } from './mute'

export async function toggleMute (account, mute) {
  if (mute) {
    (await importShowMuteDialog())(account)
  } else {
    await setAccountMuted(account.id, mute, /* notifications */ false, /* toastOnSuccess */ true)
  }
}


code:
import { paramsString } from '../_utils/ajax'
import noop from 'lodash-es/noop'
import WebSocketClient from '@gamestdio/websocket'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'

function getStreamName (timeline) {
  switch (timeline) {
    case 'local':
      return 'public:local'
    case 'federated':
      return 'public'
    case 'home':
      return 'user'
    case 'notifications':
      return 'user:notification'
    case 'direct':
      return 'direct'
  }
  if (timeline.startsWith('tag/')) {
    return 'hashtag'
  }
  if (timeline.startsWith('list/')) {
    return 'list'
  }
}

function getUrl (streamingApi, accessToken, timeline) {
  let url = `${streamingApi}/api/v1/streaming`
  let streamName = getStreamName(timeline)

  let params = {
    stream: streamName
  }

  if (timeline.startsWith('tag/')) {
    params.tag = timeline.split('/').slice(-1)[0]
  } else if (timeline.startsWith('list/')) {
    params.list = timeline.split('/').slice(-1)[0]
  }

  if (accessToken) {
    params.access_token = accessToken
  }

  return url + '?' + paramsString(params)
}

export class TimelineStream {
  constructor (streamingApi, accessToken, timeline, opts) {
    this._streamingApi = streamingApi
    this._accessToken = accessToken
    this._timeline = timeline
    this._opts = opts
    this._onStateChange = this._onStateChange.bind(this)
    this._setupWebSocket()
    this._setupLifecycle()
  }

  close () {
    this._closed = true
    this._closeWebSocket()
    this._teardownLifecycle()
  }

  _closeWebSocket () {
    if (this._ws) {
      this._ws.close()
      this._ws = null
    }
  }

  _setupWebSocket () {
    const url = getUrl(this._streamingApi, this._accessToken, this._timeline)
    const ws = new WebSocketClient(url, null, { backoff: 'fibonacci' })

    ws.onopen = this._opts.onOpen || noop
    ws.onmessage = this._opts.onMessage ? e => this._opts.onMessage(JSON.parse(e.data)) : noop
    ws.onclose = this._opts.onClose || noop
    ws.onreconnect = this._opts.onReconnect || noop

    this._ws = ws
  }

  _setupLifecycle () {
    lifecycle.addEventListener('statechange', this._onStateChange)
  }

  _teardownLifecycle () {
    lifecycle.removeEventListener('statechange', this._onStateChange)
  }

  _onStateChange (event) {
    if (this._closed) {
      return
    }
    // when the page enters or exits a frozen state, pause or resume websocket polling
    if (event.newState === 'frozen') { // page is frozen
      console.log('frozen')
      this._closeWebSocket()
    } else if (event.oldState === 'frozen') { // page is unfrozen
      console.log('unfrozen')
      this._closeWebSocket()
      this._setupWebSocket()
    }
  }
}


code:
import { auth, basename } from './utils'
import { post, WRITE_TIMEOUT } from '../_utils/ajax'

export async function blockAccount (instanceName, accessToken, accountId) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/block`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unblockAccount (instanceName, accessToken, accountId) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unblock`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { post, WRITE_TIMEOUT, paramsString, del } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function blockDomain (instanceName, accessToken, domain) {
  let url = `${basename(instanceName)}/api/v1/domain_blocks?${paramsString({ domain })}`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unblockDomain (instanceName, accessToken, domain) {
  let url = `${basename(instanceName)}/api/v1/domain_blocks?${paramsString({ domain })}`
  return del(url, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { DEFAULT_TIMEOUT, get, paramsString } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function getBlockedAccounts (instanceName, accessToken, limit = 80) {
  let url = `${basename(instanceName)}/api/v1/blocks`
  url += '?' + paramsString({ limit })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}

export async function getMutedAccounts (instanceName, accessToken, limit = 80) {
  let url = `${basename(instanceName)}/api/v1/mutes`
  url += '?' + paramsString({ limit })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}


code:
import { auth, basename } from './utils'
import { del, WRITE_TIMEOUT } from '../_utils/ajax'

export async function deleteStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}`
  return del(url, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { basename } from './utils'
import { DEFAULT_TIMEOUT, get } from '../_utils/ajax'

export async function getCustomEmoji (instanceName) {
  let url = `${basename(instanceName)}/api/v1/custom_emojis`
  return get(url, null, { timeout: DEFAULT_TIMEOUT })
}


code:
import { post, WRITE_TIMEOUT } from '../_utils/ajax'
import { basename, auth } from './utils'

export async function favoriteStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourite`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unfavoriteStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unfavourite`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { post, WRITE_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function followAccount (instanceName, accessToken, accountId) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/follow`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unfollowAccount (instanceName, accessToken, accountId) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unfollow`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function getFollows (instanceName, accessToken, accountId, limit = 80) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/following`
  url += '?' + paramsString({ limit })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}

export async function getFollowers (instanceName, accessToken, accountId, limit = 80) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/followers`
  url += '?' + paramsString({ limit })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}


code:
import { get, DEFAULT_TIMEOUT } from '../_utils/ajax'
import { basename } from './utils'

export function getInstanceInfo (instanceName) {
  let url = `${basename(instanceName)}/api/v1/instance`
  return get(url, null, { timeout: DEFAULT_TIMEOUT })
}


code:
import { get, DEFAULT_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export function getLists (instanceName, accessToken) {
  let url = `${basename(instanceName)}/api/v1/lists`
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}


code:
import { auth, basename } from './utils'
import { post, put, MEDIA_WRITE_TIMEOUT, WRITE_TIMEOUT } from '../_utils/ajax'

export async function uploadMedia (instanceName, accessToken, file, description) {
  let formData = new FormData()
  formData.append('file', file)
  if (description) {
    formData.append('description', description)
  }
  let url = `${basename(instanceName)}/api/v1/media`
  return post(url, formData, auth(accessToken), { timeout: MEDIA_WRITE_TIMEOUT })
}

export async function putMediaDescription (instanceName, accessToken, mediaId, description) {
  let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
  return put(url, { description }, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { auth, basename } from './utils'
import { post, WRITE_TIMEOUT } from '../_utils/ajax'

export async function muteAccount (instanceName, accessToken, accountId, notifications) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/mute`
  return post(url, { notifications }, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unmuteAccount (instanceName, accessToken, accountId) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/unmute`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { auth, basename } from './utils'
import { post, WRITE_TIMEOUT } from '../_utils/ajax'

export async function muteConversation (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/mute`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unmuteConversation (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unmute`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { post, paramsString, WRITE_TIMEOUT } from '../_utils/ajax'
import { basename } from './utils'

const WEBSITE = 'https://pinafore.social'
const SCOPES = 'read write follow push'
const CLIENT_NAME = 'Pinafore'

export function registerApplication (instanceName, redirectUri) {
  const url = `${basename(instanceName)}/api/v1/apps`
  return post(url, {
    client_name: CLIENT_NAME,
    redirect_uris: redirectUri,
    scopes: SCOPES,
    website: WEBSITE
  }, null, { timeout: WRITE_TIMEOUT })
}

export function generateAuthLink (instanceName, clientId, redirectUri) {
  let params = paramsString({
    'client_id': clientId,
    'redirect_uri': redirectUri,
    'response_type': 'code',
    'scope': SCOPES
  })
  return `${basename(instanceName)}/oauth/authorize?${params}`
}

export function getAccessTokenFromAuthCode (instanceName, clientId, clientSecret, code, redirectUri) {
  let url = `${basename(instanceName)}/oauth/token`
  return post(url, {
    client_id: clientId,
    client_secret: clientSecret,
    redirect_uri: redirectUri,
    grant_type: 'authorization_code',
    code: code
  }, null, { timeout: WRITE_TIMEOUT })
}


code:
import { post, WRITE_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function pinStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/pin`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

export async function unpinStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unpin`
  return post(url, null, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function getPinnedStatuses (instanceName, accessToken, accountId) {
  let url = `${basename(instanceName)}/api/v1/accounts/${accountId}/statuses`
  url += '?' + paramsString({
    limit: 40,
    pinned: true
  })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}


code:
import { get, post, DEFAULT_TIMEOUT, WRITE_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function getPoll (instanceName, accessToken, pollId) {
  let url = `${basename(instanceName)}/api/v1/polls/${pollId}`
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}

export async function voteOnPoll (instanceName, accessToken, pollId, choices) {
  let url = `${basename(instanceName)}/api/v1/polls/${pollId}/votes`
  return post(url, { choices }, auth(accessToken), { timeout: WRITE_TIMEOUT })
}


code:
import { auth, basename } from './utils'
import { post, put, get, del } from '../_utils/ajax'

export async function postSubscription (instanceName, accessToken, subscription, alerts) {
  const url = `${basename(instanceName)}/api/v1/push/subscription`

  return post(url, { subscription: subscription.toJSON(), data: { alerts } }, auth(accessToken))
}

export async function putSubscription (instanceName, accessToken, alerts) {
  const url = `${basename(instanceName)}/api/v1/push/subscription`

  return put(url, { data: { alerts } }, auth(accessToken))
}

export async function getSubscription (instanceName, accessToken) {
  const url = `${basename(instanceName)}/api/v1/push/subscription`

  return get(url, auth(accessToken))
}

export async function deleteSubscription (instanceName, accessToken) {
  const url = `${basename(instanceName)}/api/v1/push/subscription`

  return del(url, auth(accessToken))
}


code:
import { post } from '../_utils/ajax'
import { basename, auth } from './utils'

export async function reblogStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblog`
  return post(url, null, auth(accessToken))
}

export async function unreblogStatus (instanceName, accessToken, statusId) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/unreblog`
  return post(url, null, auth(accessToken))
}


code:
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'

export async function getReblogs (instanceName, accessToken, statusId, limit = 80) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/reblogged_by`
  url += '?' + paramsString({ limit })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}

export async function getFavorites (instanceName, accessToken, statusId, limit = 80) {
  let url = `${basename(instanceName)}/api/v1/statuses/${statusId}/favourited_by`
  url += '?' + paramsString({ limit })
  return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}