code:
import { dbPromise, getDatabase } from '../databaseLifecycle'
import {
  deleteFromCache, notificationsCache,
  statusesCache
} from '../cache'
import {
  NOTIFICATION_TIMELINES_STORE,
  NOTIFICATIONS_STORE, PINNED_STATUSES_STORE,
  STATUS_TIMELINES_STORE,
  STATUSES_STORE,
  THREADS_STORE
} from '../constants'
import {
  createThreadKeyRange
} from '../keys'
import { deleteAll } from '../utils'

export async function deleteStatusesAndNotifications (instanceName, statusIds, notificationIds) {
  for (let statusId of statusIds) {
    deleteFromCache(statusesCache, instanceName, statusId)
  }
  for (let notificationId of notificationIds) {
    deleteFromCache(notificationsCache, instanceName, notificationId)
  }
  const db = await getDatabase(instanceName)
  let storeNames = [
    STATUSES_STORE,
    STATUS_TIMELINES_STORE,
    NOTIFICATIONS_STORE,
    NOTIFICATION_TIMELINES_STORE,
    PINNED_STATUSES_STORE,
    THREADS_STORE
  ]
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
    let [
      statusesStore,
      statusTimelinesStore,
      notificationsStore,
      notificationTimelinesStore,
      pinnedStatusesStore,
      threadsStore
    ] = stores

    function deleteStatus (statusId) {
      statusesStore.delete(statusId)
      deleteAll(
        pinnedStatusesStore,
        pinnedStatusesStore.index('statusId'),
        IDBKeyRange.only(statusId)
      )
      deleteAll(
        statusTimelinesStore,
        statusTimelinesStore.index('statusId'),
        IDBKeyRange.only(statusId)
      )
      deleteAll(
        threadsStore,
        threadsStore.index('statusId'),
        IDBKeyRange.only(statusId)
      )
      deleteAll(
        threadsStore,
        threadsStore,
        createThreadKeyRange(statusId)
      )
    }

    function deleteNotification (notificationId) {
      notificationsStore.delete(notificationId)
      deleteAll(
        notificationTimelinesStore,
        notificationTimelinesStore.index('notificationId'),
        IDBKeyRange.only(notificationId)
      )
    }

    for (let statusId of statusIds) {
      deleteStatus(statusId)
    }
    for (let notificationId of notificationIds) {
      deleteNotification(notificationId)
    }
  })
}


code:
export function fetchAccount (accountsStore, id, callback) {
  accountsStore.get(id).onsuccess = e => {
    callback(e.target.result)
  }
}

repo_name: RayoGundead/pinny

license: mit


code:
import { fetchAccount } from './fetchAccount'
import { ACCOUNT_ID, STATUS_ID } from '../constants'
import { fetchStatus } from './fetchStatus'

export function fetchNotification (notificationsStore, statusesStore, accountsStore, id, callback) {
  notificationsStore.get(id).onsuccess = e => {
    let notification = e.target.result
    callback(notification)
    fetchAccount(accountsStore, notification[ACCOUNT_ID], account => {
      notification.account = account
    })
    if (notification[STATUS_ID]) {
      fetchStatus(statusesStore, accountsStore, notification[STATUS_ID], status => {
        notification.status = status
      })
    }
  }
}


code:
import { fetchAccount } from './fetchAccount'
import { ACCOUNT_ID, REBLOG_ID } from '../constants'

export function fetchStatus (statusesStore, accountsStore, id, callback) {
  statusesStore.get(id).onsuccess = e => {
    let status = e.target.result
    callback(status)
    if (!status) {
      return
    }
    fetchAccount(accountsStore, status[ACCOUNT_ID], account => {
      status.account = account
    })
    if (status[REBLOG_ID]) {
      fetchStatus(statusesStore, accountsStore, status[REBLOG_ID], reblog => {
        status.reblog = reblog
      })
    }
  }
}


code:
import { dbPromise, getDatabase } from '../databaseLifecycle'
import { getInCache, hasInCache, notificationsCache, setInCache, statusesCache } from '../cache'
import {
  ACCOUNTS_STORE,
  NOTIFICATIONS_STORE,
  STATUSES_STORE
} from '../constants'
import { fetchStatus } from './fetchStatus'
import { fetchNotification } from './fetchNotification'

export async function getStatus (instanceName, id) {
  if (hasInCache(statusesCache, instanceName, id)) {
    return getInCache(statusesCache, instanceName, id)
  }
  const db = await getDatabase(instanceName)
  let storeNames = [STATUSES_STORE, ACCOUNTS_STORE]
  let result = await dbPromise(db, storeNames, 'readonly', (stores, callback) => {
    let [ statusesStore, accountsStore ] = stores
    fetchStatus(statusesStore, accountsStore, id, callback)
  })
  setInCache(statusesCache, instanceName, id, result)
  return result
}

export async function getNotification (instanceName, id) {
  if (hasInCache(notificationsCache, instanceName, id)) {
    return getInCache(notificationsCache, instanceName, id)
  }
  const db = await getDatabase(instanceName)
  let storeNames = [NOTIFICATIONS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  let result = await dbPromise(db, storeNames, 'readonly', (stores, callback) => {
    let [ notificationsStore, statusesStore, accountsStore ] = stores
    fetchNotification(notificationsStore, statusesStore, accountsStore, id, callback)
  })
  setInCache(notificationsCache, instanceName, id, result)
  return result
}


code:
import difference from 'lodash-es/difference'
import times from 'lodash-es/times'
import { cloneForStorage } from '../helpers'
import { dbPromise, getDatabase } from '../databaseLifecycle'
import { accountsCache, notificationsCache, setInCache, statusesCache } from '../cache'
import { scheduleCleanup } from '../cleanup'
import {
  ACCOUNTS_STORE,
  NOTIFICATION_TIMELINES_STORE,
  NOTIFICATIONS_STORE,
  STATUS_TIMELINES_STORE,
  STATUSES_STORE,
  THREADS_STORE
} from '../constants'
import {
  createThreadId,
  createThreadKeyRange,
  createTimelineId
} from '../keys'
import { cacheStatus } from './cacheStatus'

export function putStatus (statusesStore, status) {
  statusesStore.put(cloneForStorage(status))
}

export function putAccount (accountsStore, account) {
  accountsStore.put(cloneForStorage(account))
}

export function putNotification (notificationsStore, notification) {
  notificationsStore.put(cloneForStorage(notification))
}

export function storeAccount (accountsStore, account) {
  putAccount(accountsStore, account)
}

export function storeStatus (statusesStore, accountsStore, status) {
  putStatus(statusesStore, status)
  putAccount(accountsStore, status.account)
  if (status.reblog) {
    putStatus(statusesStore, status.reblog)
    putAccount(accountsStore, status.reblog.account)
  }
}

export function storeNotification (notificationsStore, statusesStore, accountsStore, notification) {
  if (notification.status) {
    storeStatus(statusesStore, accountsStore, notification.status)
  }
  storeAccount(accountsStore, notification.account)
  putNotification(notificationsStore, notification)
}

async function insertTimelineNotifications (instanceName, timeline, notifications) {
  for (let notification of notifications) {
    setInCache(notificationsCache, instanceName, notification.id, notification)
    setInCache(accountsCache, instanceName, notification.account.id, notification.account)
    if (notification.status) {
      setInCache(statusesCache, instanceName, notification.status.id, notification.status)
    }
  }
  const db = await getDatabase(instanceName)
  let storeNames = [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, ACCOUNTS_STORE, STATUSES_STORE]
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
    let [ timelineStore, notificationsStore, accountsStore, statusesStore ] = stores
    for (let notification of notifications) {
      storeNotification(notificationsStore, statusesStore, accountsStore, notification)
      timelineStore.put(notification.id, createTimelineId(timeline, notification.id))
    }
  })
}

async function insertTimelineStatuses (instanceName, timeline, statuses) {
  for (let status of statuses) {
    cacheStatus(status, instanceName)
  }
  const db = await getDatabase(instanceName)
  let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
    let [ timelineStore, statusesStore, accountsStore ] = stores
    for (let status of statuses) {
      storeStatus(statusesStore, accountsStore, status)
      timelineStore.put(status.id, createTimelineId(timeline, status.id))
    }
  })
}

async function insertStatusThread (instanceName, statusId, statuses) {
  for (let status of statuses) {
    cacheStatus(status, instanceName)
  }
  const db = await getDatabase(instanceName)
  let storeNames = [THREADS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
    let [ threadsStore, statusesStore, accountsStore ] = stores
    threadsStore.getAllKeys(createThreadKeyRange(statusId)).onsuccess = e => {
      let existingKeys = e.target.result
      let newKeys = times(statuses.length, i => createThreadId(statusId, i))
      let keysToDelete = difference(existingKeys, newKeys)
      for (let key of keysToDelete) {
        threadsStore.delete(key)
      }
    }
    statuses.forEach((otherStatus, i) => {
      storeStatus(statusesStore, accountsStore, otherStatus)
      threadsStore.put(otherStatus.id, createThreadId(statusId, i))
    })
  })
}

export async function insertTimelineItems (instanceName, timeline, timelineItems) {
  /* no await */ scheduleCleanup()
  if (timeline === 'notifications' || timeline === 'notifications/mentions') {
    return insertTimelineNotifications(instanceName, timeline, timelineItems)
  } else if (timeline.startsWith('status/')) {
    let statusId = timeline.split('/').slice(-1)[0]
    return insertStatusThread(instanceName, statusId, timelineItems)
  } else {
    return insertTimelineStatuses(instanceName, timeline, timelineItems)
  }
}


code:
import { dbPromise, getDatabase } from '../databaseLifecycle'
import { STATUSES_STORE, STATUS_ID, REBLOG_ID, NOTIFICATIONS_STORE } from '../constants'

export async function getReblogsForStatus (instanceName, id) {
  const db = await getDatabase(instanceName)
  await dbPromise(db, STATUSES_STORE, 'readonly', (statusesStore, callback) => {
    statusesStore.index(REBLOG_ID).getAll(IDBKeyRange.only(id)).onsuccess = e => {
      callback(e.target.result)
    }
  })
}

export async function getNotificationIdsForStatuses (instanceName, statusIds) {
  const db = await getDatabase(instanceName)
  return dbPromise(db, NOTIFICATIONS_STORE, 'readonly', (notificationsStore, callback) => {
    let res = []
    callback(res)
    statusIds.forEach(statusId => {
      let req = notificationsStore.index(STATUS_ID).getAllKeys(IDBKeyRange.only(statusId))
      req.onsuccess = e => {
        for (let id of e.target.result) {
          res.push(id)
        }
      }
    })
  })
}


code:
import { dbPromise, getDatabase } from '../databaseLifecycle'
import {
  ACCOUNTS_STORE,
  NOTIFICATION_TIMELINES_STORE,
  NOTIFICATIONS_STORE,
  STATUS_TIMELINES_STORE,
  STATUSES_STORE,
  THREADS_STORE
} from '../constants'
import {
  createThreadKeyRange,
  createTimelineKeyRange
} from '../keys'
import { fetchStatus } from './fetchStatus'
import { fetchNotification } from './fetchNotification'
import { TIMELINE_BATCH_SIZE } from '../../_static/timelines'

async function getNotificationTimeline (instanceName, timeline, maxId, limit) {
  let storeNames = [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  const db = await getDatabase(instanceName)
  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
    let [ timelineStore, notificationsStore, statusesStore, accountsStore ] = stores
    let keyRange = createTimelineKeyRange(timeline, maxId)

    timelineStore.getAll(keyRange, limit).onsuccess = e => {
      let timelineResults = e.target.result
      let res = new Array(timelineResults.length)
      timelineResults.forEach((notificationId, i) => {
        fetchNotification(notificationsStore, statusesStore, accountsStore, notificationId, notification => {
          res[i] = notification
        })
      })
      callback(res)
    }
  })
}

async function getStatusTimeline (instanceName, timeline, maxId, limit) {
  let storeNames = [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  const db = await getDatabase(instanceName)
  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
    let [ timelineStore, statusesStore, accountsStore ] = stores
    let getReq = timelineStore.getAll(createTimelineKeyRange(timeline, maxId), limit)
    getReq.onsuccess = e => {
      let timelineResults = e.target.result
      let res = new Array(timelineResults.length)
      timelineResults.forEach((statusId, i) => {
        fetchStatus(statusesStore, accountsStore, statusId, status => {
          res[i] = status
        })
      })
      callback(res)
    }
  })
}

async function getStatusThread (instanceName, statusId) {
  let storeNames = [THREADS_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  const db = await getDatabase(instanceName)
  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
    let [ threadsStore, statusesStore, accountsStore ] = stores
    let keyRange = createThreadKeyRange(statusId)
    threadsStore.getAll(keyRange).onsuccess = e => {
      let thread = e.target.result
      if (thread.length) {
        let res = new Array(thread.length)
        callback(res)
        thread.forEach((otherStatusId, i) => {
          fetchStatus(statusesStore, accountsStore, otherStatusId, status => {
            res[i] = status
          })
        })
      } else {
        // thread not cached; just make a "fake" thread with only one status in it
        fetchStatus(statusesStore, accountsStore, statusId, status => {
          let res = [status]
          callback(res)
        })
      }
    }
  })
}

export async function getTimeline (instanceName, timeline, maxId, limit) {
  maxId = maxId || null
  limit = limit || TIMELINE_BATCH_SIZE
  if (timeline === 'notifications' || timeline === 'notifications/mentions') {
    return getNotificationTimeline(instanceName, timeline, maxId, limit)
  } else if (timeline.startsWith('status/')) {
    let statusId = timeline.split('/').slice(-1)[0]
    return getStatusThread(instanceName, statusId)
  } else {
    return getStatusTimeline(instanceName, timeline, maxId, limit)
  }
}


code:
import { cacheStatus } from './cacheStatus'
import { getDatabase, dbPromise } from '../databaseLifecycle'
import { PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE } from '../constants'
import { createPinnedStatusId, createPinnedStatusKeyRange } from '../keys'
import { storeStatus } from './insertion'
import { fetchStatus } from './fetchStatus'

export async function insertPinnedStatuses (instanceName, accountId, statuses) {
  for (let status of statuses) {
    cacheStatus(status, instanceName)
  }
  const db = await getDatabase(instanceName)
  let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores

    let keyRange = createPinnedStatusKeyRange(accountId)
    pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
      // if there was e.g. 1 pinned status before and 2 now, then we need to delete the old one
      let existingPinnedStatuses = e.target.result
      for (let i = statuses.length; i < existingPinnedStatuses.length; i++) {
        pinnedStatusesStore.delete(createPinnedStatusKeyRange(accountId, i))
      }
      statuses.forEach((status, i) => {
        storeStatus(statusesStore, accountsStore, status)
        pinnedStatusesStore.put(status.id, createPinnedStatusId(accountId, i))
      })
    }
  })
}

export async function getPinnedStatuses (instanceName, accountId) {
  let storeNames = [PINNED_STATUSES_STORE, STATUSES_STORE, ACCOUNTS_STORE]
  const db = await getDatabase(instanceName)
  return dbPromise(db, storeNames, 'readonly', (stores, callback) => {
    let [ pinnedStatusesStore, statusesStore, accountsStore ] = stores
    let keyRange = createPinnedStatusKeyRange(accountId)
    pinnedStatusesStore.getAll(keyRange).onsuccess = e => {
      let pinnedResults = e.target.result
      let res = new Array(pinnedResults.length)
      pinnedResults.forEach((statusId, i) => {
        fetchStatus(statusesStore, accountsStore, statusId, status => {
          res[i] = status
        })
      })
      callback(res)
    }
  })
}


code:
import { dbPromise, getDatabase } from '../databaseLifecycle'
import { getInCache, hasInCache, statusesCache } from '../cache'
import { STATUSES_STORE } from '../constants'
import { cacheStatus } from './cacheStatus'
import { putStatus } from './insertion'

//
// update statuses
//

async function updateStatus (instanceName, statusId, updateFunc) {
  const db = await getDatabase(instanceName)
  if (hasInCache(statusesCache, instanceName, statusId)) {
    let status = getInCache(statusesCache, instanceName, statusId)
    updateFunc(status)
    cacheStatus(status, instanceName)
  }
  return dbPromise(db, STATUSES_STORE, 'readwrite', (statusesStore) => {
    statusesStore.get(statusId).onsuccess = e => {
      let status = e.target.result
      updateFunc(status)
      putStatus(statusesStore, status)
    }
  })
}

export async function setStatusFavorited (instanceName, statusId, favorited) {
  return updateStatus(instanceName, statusId, status => {
    let delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
    status.favourited = favorited
    status.favourites_count = (status.favourites_count || 0) + delta
  })
}

export async function setStatusReblogged (instanceName, statusId, reblogged) {
  return updateStatus(instanceName, statusId, status => {
    let delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0)
    status.reblogged = reblogged
    status.reblogs_count = (status.reblogs_count || 0) + delta
  })
}

export async function setStatusPinned (instanceName, statusId, pinned) {
  return updateStatus(instanceName, statusId, status => {
    status.pinned = pinned
  })
}

export async function setStatusMuted (instanceName, statusId, muted) {
  return updateStatus(instanceName, statusId, status => {
    status.muted = muted
  })
}


code:
export function deleteAll (store, index, keyRange) {
  index.getAllKeys(keyRange).onsuccess = e => {
    for (let result of e.target.result) {
      store.delete(result)
    }
  }
}


code:
<svelte:head>
  <title>{status}</title>
</svelte:head>

{status}

{error.message}


code:
import { format } from '../_thirdparty/timeago/timeago'
import { mark, stop } from '../_utils/marks'

// Format a date in the past
export function formatTimeagoDate (date, now) {
  mark('formatTimeagoDate')
  // use Math.max() to avoid things like "in 10 seconds" when the timestamps are slightly off
  let res = format(date, Math.max(now, date))
  stop('formatTimeagoDate')
  return res
}

// Format a date in the future
export function formatTimeagoFutureDate (date, now) {
  mark('formatTimeagoFutureDate')
  // use Math.min() for same reason as above
  let res = format(date, Math.min(now, date))
  stop('formatTimeagoFutureDate')
  return res
}


code:
<Nav {page} />

<div class="main-content">
  <main class="{infiniteScrollPage ? 'infinite-scroll-page' : ''}">
    <svelte:component this={child.component} {...child.props} />
  </main>
  {#if !$isUserLoggedIn && page === 'home'}
  <InformationalFooter />
  {/if}
</div>
<style>
  /* this avoids a flash of the background color when switching timelines */
  .infinite-scroll-page {
    min-height: 100vh;
  }
</style>
<script>
  import { store } from './_store/store'
  import { observe } from 'svelte-extras'

  export default {
    components: {
      Nav: './_components/Nav.html',
      InformationalFooter: './_components/InformationalFooter.html'
    },
    oncreate () {
      if (process.browser && process.env.NODE_ENV !== 'production') {
        window.layoutObject = this
      }

      this.observe('page', page => {
        console.log('currentPage', page)
        this.store.set({ currentPage: page })
      })
    },
    store: () => store,
    computed: {
      page: ({ child }) => child.props.path.slice(1) || 'home',
      infiniteScrollPage: ({ $isUserLoggedIn, page }) => $isUserLoggedIn && !page.startsWith('settings')
    },
    methods: {
      observe
    }
  }
</script>


code:
<DynamicPageBanner title="Followers" />
<AccountsListPage {accountsFetcher} />
<script>
  import { getFollowers } from '../../../_api/followsAndFollowers'
  import AccountsListPage from '../../../_components/AccountsListPage.html'
  import { store } from '../../../_store/store'
  import DynamicPageBanner from '../../../_components/DynamicPageBanner.html'

  export default {
    computed: {
      accountId: ({ params }) => params.accountId,
      accountsFetcher: ({ $currentInstance, $accessToken, accountId }) => () => getFollowers($currentInstance, $accessToken, accountId)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
<DynamicPageBanner title="Follows" />
<AccountsListPage {accountsFetcher} />
<script>
  import { getFollows } from '../../../_api/followsAndFollowers'
  import AccountsListPage from '../../../_components/AccountsListPage.html'
  import { store } from '../../../_store/store'
  import DynamicPageBanner from '../../../_components/DynamicPageBanner.html'

  export default {
    computed: {
      accountId: ({ params }) => params.accountId,
      accountsFetcher: ({ $currentInstance, $accessToken, accountId }) => () => getFollows($currentInstance, $accessToken, accountId)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
<AccountProfilePage accountId={params.accountId} filter="" />
<script>
  import AccountProfilePage from '../../../_components/profile/AccountProfilePage.html'

  export default {
    components: {
      AccountProfilePage
    }
  }
</script>


code:
<AccountProfilePage accountId={params.accountId} filter="media" />
<script>
  import AccountProfilePage from '../../../_components/profile/AccountProfilePage.html'

  export default {
    components: {
      AccountProfilePage
    }
  }
</script>


code:
<AccountProfilePage accountId={params.accountId} filter="with_replies" />
<script>
  import AccountProfilePage from '../../../_components/profile/AccountProfilePage.html'

  export default {
    components: {
      AccountProfilePage
    }
  }
</script>


code:
<DynamicPageBanner title="Blocked users" icon="#fa-ban" />
{#if $isUserLoggedIn }
  <AccountsListPage {accountsFetcher} {accountActions} />
{/if}
<script>
  import AccountsListPage from '../_components/AccountsListPage.html'
  import { store } from '../_store/store'
  import { getBlockedAccounts } from '../_api/blockedAndMuted'
  import DynamicPageBanner from '../_components/DynamicPageBanner.html'
  import { setAccountBlocked } from '../_actions/block'

  export default {
    data: () => ({
      accountActions: [
        {
          icon: '#fa-unlock',
          label: 'Unblock',
          onclick: (accountId) => setAccountBlocked(accountId, false, true)
        }
      ]
    }),
    computed: {
      accountsFetcher: ({ $currentInstance, $accessToken }) => () => getBlockedAccounts($currentInstance, $accessToken)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
{#if $isUserLoggedIn}

    Timelines

  <PageList label="Timelines">
    <PageListItem href="/local"
                  label="Local Timeline"
                  icon="#fa-users"
                  pinnable="true"
    />
    <PageListItem href="/federated"
                  label="Federated Timeline"
                  icon="#fa-globe"
                  pinnable="true"
    />
    <PageListItem href="/favorites"
                  label="Favorites"
                  icon="#fa-star"
                  pinnable="true"
    />
    <PageListItem href="/direct"
                  label="Direct messages"
                  icon="#fa-envelope"
                  pinnable="true"
    />
  </PageList>

  {#if $lists.length}

      Lists

    <PageList label="Lists">
      {#each $lists as list}
      <PageListItem href="/lists/{list.id}"
                    label={list.title}
                    icon="#fa-bars"
                    pinnable="true"
      />
      {/each}
    </PageList>

  {/if}

    Instance settings

  <PageList label="Instance settings">
    {#if isLockedAccount}
    <PageListItem href="/requests"
                  label="Follow requests"
                  icon="#fa-user-plus"
     />
    {/if}
    <PageListItem href="/muted"
                  label="Muted users"
                  icon="#fa-volume-off"
    />
    <PageListItem href="/blocked"
                  label="Blocked users"
                  icon="#fa-ban"
    />
    <PageListItem href="/pinned"
                  label="Pinned toots"
                  icon="#fa-thumb-tack"
    />
  </PageList>

{:else}
<HiddenFromSSR>
  <FreeTextLayout>

Community

Community options appear here when logged in.

  </FreeTextLayout>
</HiddenFromSSR>

    Muted
    Blocked
    Pinned
    Requests
    Share
    Federated
    Favorites
    Conversations

{/if}


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="direct">
    {#if $pinnedPage !== '/direct'}
      <DynamicPageBanner title="Direct messages" icon="#fa-envelope"/>
    {/if}
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

Conversations

Your conversations will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="favorites">
    {#if $pinnedPage !== '/favorites'}
      <DynamicPageBanner title="Favorites" icon="#fa-star"/>
    {/if}
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

Favorites

Your favorites will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="federated">
    {#if $pinnedPage !== '/federated'}
      <DynamicPageBanner title="Federated Timeline" icon="#fa-globe"/>
    {/if}
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

Federated

Your federated timeline will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
{#if $isUserLoggedIn}
  <TimelineHomePage/>
{:else}
  <NotLoggedInHome/>
{/if}
<script>
  import NotLoggedInHome from '../_components/NotLoggedInHome.html'
  import { store } from '../_store/store.js'
  import TimelineHomePage from '../_components/TimelineHomePage.html'
  import { observe } from 'svelte-extras'
  import { showShareDialogIfNecessary } from '../_actions/showShareDialogIfNecessary'

  export default {
    async oncreate () {
      let observed = false
      this.observe('currentVerifyCredentials', verifyCredentials => {
        if (verifyCredentials && !observed) {
          // when the verifyCredentials object is available, we can check to see
          // if the user is trying to share something, then share it
          observed = true
          /* no await */ showShareDialogIfNecessary()
        }
      })
    },
    store: () => store,
    computed: {
      currentVerifyCredentials: ({ $currentVerifyCredentials }) => $currentVerifyCredentials
    },
    methods: {
      observe
    },
    components: {
      NotLoggedInHome,
      TimelineHomePage
    }
  }
</script>


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="list/{params.listId}">
    {#if $pinnedPage !== `/lists/${params.listId}`}
      <DynamicPageBanner title={listTitle} icon="#fa-bars"/>
    {/if}
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

List

A list will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="local">
    {#if $pinnedPage !== '/local'}
      <DynamicPageBanner title="Local Timeline" icon="#fa-users"/>
    {/if}
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

Local

Your local timeline will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
<DynamicPageBanner title="Muted users" icon="#fa-volume-off" />
{#if $isUserLoggedIn }
  <AccountsListPage {accountsFetcher} {accountActions} />
{/if}
<script>
  import AccountsListPage from '../_components/AccountsListPage.html'
  import { store } from '../_store/store'
  import { getMutedAccounts } from '../_api/blockedAndMuted'
  import DynamicPageBanner from '../_components/DynamicPageBanner.html'
  import { setAccountMuted } from '../_actions/mute'

  export default {
    data: () => ({
      accountActions: [
        {
          icon: '#fa-volume-up',
          label: 'Unmute',
          onclick: (accountId) => setAccountMuted(accountId,
            /* mute */ false,
            /* notifications */ false,
            /* toastOnSuccess */ true)
        }
      ]
    }),
    computed: {
      accountsFetcher: ({ $currentInstance, $accessToken }) => () => getMutedAccounts($currentInstance, $accessToken)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
{#if $isUserLoggedIn}
  <NotificationFilters filter="" />
  <TimelinePage timeline="notifications" />
{:else}
<HiddenFromSSR>
  <FreeTextLayout>

Notifications

Your notifications will appear here when logged in.

  </FreeTextLayout>

    Notification mentions

</HiddenFromSSR>
{/if}


code:
{#if $isUserLoggedIn}
  <NotificationFilters filter="mentions" />
  <TimelinePage timeline="notifications/mentions" />
{:else}
<HiddenFromSSR>
  <FreeTextLayout>

Notification mentions

Your notification mentions will appear here when logged in.

  </FreeTextLayout>
</HiddenFromSSR>
{/if}


code:
<DynamicPageBanner title="Pinned toots" icon="#fa-thumb-tack" />
{#if $isUserLoggedIn }

    {#if loading}
    <LoadingPage />
    {:elseif statuses && statuses.length}

      {#each statuses as status, index}
      <StatusSearchResult {status} {index} length={statuses.length} />
      {/each}

    {/if}

{/if}


code:
<DynamicPageBanner title="Follow requests" icon="#fa-user-plus" />
{#if $isUserLoggedIn }
  <AccountsListPage {accountsFetcher} {accountActions} />
{/if}
<script>
  import AccountsListPage from '../_components/AccountsListPage.html'
  import { store } from '../_store/store'
  import { getFollowRequests } from '../_actions/followRequests'
  import DynamicPageBanner from '../_components/DynamicPageBanner.html'
  import { setFollowRequestApprovedOrRejected } from '../_actions/requests'

  export default {
    data: () => ({
      accountActions: [
        {
          icon: '#fa-check',
          label: 'Approve',
          onclick: (accountId) => setFollowRequestApprovedOrRejected(accountId, true, true)
        },
        {
          icon: '#fa-times',
          label: 'Reject',
          onclick: (accountId) => setFollowRequestApprovedOrRejected(accountId, false, true)
        }
      ]
    }),
    computed: {
      accountsFetcher: ({ $currentInstance, $accessToken }) => () => getFollowRequests($currentInstance, $accessToken)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
{#if $isUserLoggedIn}

  <Search></Search>

{:else}
<HiddenFromSSR>
  <FreeTextLayout>

Search

You can search once logged in to an instance.

  </FreeTextLayout>
</HiddenFromSSR>
{/if}


code:
<SettingsLayout page='settings/about' label="About Pinafore">

About Pinafore

Version {version}

    Pinafore is <ExternalLink href="https://github.com/nolanlawson/pinafore">free and open-source software</ExternalLink>
    created by <ExternalLink href="https://nolanlawson.com">Nolan Lawson</ExternalLink> and distributed under the
    <ExternalLink href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</ExternalLink>.

Privacy Policy

    Pinafore does not store any personal information on its servers, including but not limited to names, email addresses,
    IP addresses, posts, and photos.

    Pinafore is a static site. All data is stored locally in your browser and shared with the fediverse
    instance(s) you connect to.

Credits

    Icons provided by <ExternalLink href="http://fontawesome.io/">Font Awesome</ExternalLink>.

    Logo thanks to "sailboat" by Gregor Cresnar from <ExternalLink href="https://thenounproject.com/">the Noun Project</ExternalLink>.

</SettingsLayout>


code:
<SettingsLayout page='settings/general' label="General">

General Settings

Media

  <form class="ui-settings">

      <input type="checkbox" id="choice-never-mark-media-sensitive"
             bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
      Show sensitive media by default

      <input type="checkbox" id="choice-mark-media-sensitive"
             bind:checked="$markMediaAsSensitive" on:change="onChange(event)">
      Hide all media by default

      <input type="checkbox" id="choice-large-inline-media"
             bind:checked="$largeInlineMedia" on:change="onChange(event)">
      Show large inline images and videos

      <input type="checkbox" id="choice-autoplay-gif"
             bind:checked="$autoplayGifs" on:change="onChange(event)">
      Autoplay GIFs

  </form>

UI

  <form class="ui-settings">

      <input type="checkbox" id="choice-disable-custom-scrollbars"
             bind:checked="$disableCustomScrollbars" on:change="onChange(event)">
      Disable custom scrollbars

      <input type="checkbox" id="choice-disable-infinite-scroll"
             bind:checked="$disableInfiniteScroll" on:change="onChange(event)">
      Disable
        <Tooltip
          text="infinite scroll"
          tooltipText={
            "When infinite scroll is disabled, new toots will not automatically appear at the bottom " +
            "or top of the timeline. Instead, buttons will allow you to load more content on demand."
          }
        />

      <input type="checkbox" id="choice-hide-cards"
             bind:checked="$hideCards" on:change="onChange(event)">
      Hide link preview cards

      <input type="checkbox" id="choice-underline-links"
             bind:checked="$underlineLinks" on:change="onChange(event)">
      Underline links in toots and profiles

  </form>

Accessibility

  <form class="ui-settings">

      <input type="checkbox" id="choice-reduce-motion"
             bind:checked="$reduceMotion" on:change="onChange(event)">
      Reduce motion in UI animations

      <input type="checkbox" id="choice-disable-tap-on-status"
             bind:checked="$disableTapOnStatus" on:change="onChange(event)">
      Disable tappable area on entire toot

      <input type="checkbox" id="choice-omit-emoji-in-display-names"
             bind:checked="$omitEmojiInDisplayNames" on:change="onChange(event)">
      Remove emoji from user display names

      <input type="checkbox" id="choice-disable-long-aria-labels"
             bind:checked="$disableLongAriaLabels" on:change="onChange(event)">
      Use short article ARIA labels

  </form>

  {#if $isUserLoggedIn }

{themeTitle}

    <ThemeSettings instanceName={$currentInstance} />
  {/if}
</SettingsLayout>


code:
<SettingsLayout page='settings/hotkeys' label="Hotkeys">

Hotkeys

Preferences

  <form class="ui-settings" aria-label="Hotkey settings">

      <input type="checkbox" id="choice-disable-hotkeys"
             bind:checked="$disableHotkeys" on:change="onChange()">
      Disable hotkeys

  </form>

Guide

  <ShortcutHelpInfo />
</SettingsLayout>


code:
<SettingsLayout page='settings' label="Settings">

Settings

  <SettingsList>
    <SettingsListRow>
      <SettingsListButton href="/settings/general" label="General"/>
    </SettingsListRow>
    <SettingsListRow>
      <SettingsListButton href="/settings/instances" label="Instances"/>
    </SettingsListRow>
    <SettingsListRow>
      <SettingsListButton href="/settings/hotkeys" label="Hotkeys"/>
    </SettingsListRow>
    <SettingsListRow>
    <SettingsListButton href="#" on:click="reload(event)" label="Reload"/>
    </SettingsListRow>
    <SettingsListRow>
      <SettingsListButton href="/settings/about" label="About Pinafore"/>
    </SettingsListRow>
  </SettingsList>

</SettingsLayout>


code:
<SettingsLayout page='settings/instances/{params.instanceName}' label={params.instanceName}>

{params.instanceName}

  {#if verifyCredentials}

Logged in as:

    <InstanceUserProfile {verifyCredentials} />

Home timeline filters

    <HomeTimelineFilterSettings {instanceName} />

Notification filters

    <NotificationFilterSettings {instanceName} />

Push notifications

    <PushNotificationSettings {instanceName} />

Theme

    <ThemeSettings {instanceName} />

    <InstanceActions {instanceName} />
  {/if}
</SettingsLayout>


code:
<SettingsLayout page='settings/instances/add' label="Add instance">

Add instance

  <form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">

    {#if !hasIndexedDB || !hasLocalStorage}

        It seems Pinafore cannot store data locally. Is your browser in private mode
        or blocking cookies? Pinafore stores all data locally, and requires LocalStorage and
        IndexedDB to work correctly.

    {/if}

    {#if $logInToInstanceError && $logInToInstanceErrorForText === $instanceNameInSearch}

        Error: {$logInToInstanceError}

    {/if}

        You must enable JavaScript to log in.

    Instance:
    <input class="add-new-instance-input" type="text" inputmode="url" id="instanceInput"
           bind:value='$instanceNameInSearch' placeholder="Enter instance name" required
    >
    <button class="primary add-new-instance-button" type="submit" id="submitButton"
            disabled={!$instanceNameInSearch || $logInToInstanceLoading}>
      Log in
    </button>
  </form>

  {#if !$isUserLoggedIn}

    Don't have an
    <Tooltip
      text="instance"
      tooltipText="An instance is your Mastodon home server, such as mastodon.social or cybre.space."
    />
    ?
    <ExternalLink href="https://joinmastodon.org">Join Mastodon!</ExternalLink>

  {/if}
</SettingsLayout>


code:
<SettingsLayout page='settings/instances' label="Instances">

Instances

  {#if $isUserLoggedIn}

Instances you've logged in to:

  <SettingsList label="Instances">
    {#each instanceStates as instance}
    <SettingsListRow>
      <SettingsListButton className="instance-switcher-instance-name"
                          href="/settings/instances/{instance.name}"
                          label={instance.name}
                          ariaLabel={instance.label} />

        <button class="instance-switcher-button"
                aria-label={instance.switchLabel}
                title={instance.switchLabel}
                aria-pressed={instance.current}
                on:click="onSwitchToThisInstance(event, instance.name)">
          <SvgIcon className="instance-switcher-button-svg"
                   href={instance.current ? '#fa-star' : '#fa-star-o'} />
        </button>

    </SettingsListRow>
    {/each}
  </SettingsList>

Add another instance

  {:else}

You're not logged in to any instances.

Log in to an instance to start using Pinafore.

  {/if}
</SettingsLayout>


code:
<DynamicPageBanner title="Favorites" icon="#fa-star" />
<AccountsListPage {accountsFetcher} />
<script>
  import { getFavorites } from '../../../_api/reblogsAndFavs'
  import AccountsListPage from '../../../_components/AccountsListPage.html'
  import { store } from '../../../_store/store'
  import DynamicPageBanner from '../../../_components/DynamicPageBanner.html'

  export default {
    computed: {
      statusId: ({ params }) => params.statusId,
      accountsFetcher: ({ $currentInstance, $accessToken, statusId }) => () => getFavorites($currentInstance, $accessToken, statusId)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="status/{params.statusId}">
    <DynamicPageBanner title="" ariaTitle="Status thread page" />
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

Status

A status thread will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
<DynamicPageBanner title="Boosts" icon="#fa-retweet" />
<AccountsListPage {accountsFetcher} />
<script>
  import { getReblogs } from '../../../_api/reblogsAndFavs'
  import AccountsListPage from '../../../_components/AccountsListPage.html'
  import { store } from '../../../_store/store'
  import DynamicPageBanner from '../../../_components/DynamicPageBanner.html'

  export default {
    computed: {
      statusId: ({ params }) => params.statusId,
      accountsFetcher: ({ $currentInstance, $accessToken, statusId }) => () => getReblogs($currentInstance, $accessToken, statusId)
    },
    store: () => store,
    components: {
      AccountsListPage,
      DynamicPageBanner
    }
  }
</script>


code:
{#if $isUserLoggedIn}
  <TimelinePage timeline="tag/{params.tagName}">
    <DynamicPageBanner title="{'#' + params.tagName}"/>
  </TimelinePage>
{:else}
  <HiddenFromSSR>
    <FreeTextLayout>

#{params.tagName}

A hashtag timeline will appear here when logged in.

    </FreeTextLayout>
  </HiddenFromSSR>
{/if}


code:
import { importEmojiMart } from '../_utils/asyncModules'
import { loadCSS } from '../_utils/loadCSS'

async function fetchEmojiMartData () {
  return (await fetch('/emoji-mart-all.json')).json()
}

let Picker // cache so we don't have to recreate every time

export async function createEmojiMartPicker () {
  if (!Picker) {
    loadCSS('/emoji-mart.css')
    let [data, createEmojiMartPickerFromData] = await Promise.all([
      fetchEmojiMartData(),
      importEmojiMart()
    ])
    Picker = createEmojiMartPickerFromData(data)
  }
  return Picker
}


code:
// I wrap the emoji-mart React code itself here, so that we don't need to pass in a huge "data"
// object via a JSON-stringified custom element attribute. Also, AFAICT there is no way when
// using `remount` to pass in functions as attributes, since everything is stringified. So
// I just fire a global event here when an emoji is clicked.

import NimblePicker from 'emoji-mart/dist-modern/components/picker/nimble-picker'
import React from 'react'
import { emit } from '../_utils/eventBus'

function onEmojiSelected (emoji) {
  emit('emoji-selected', emoji)
}

export default function createEmojiMartPickerFromData (data) {
  return props => React.createElement(NimblePicker, Object.assign({
    set: 'twitter', // same as Mastodon frontend
    data, // same as Mastodon frontend
    native: true,
    onSelect: onEmojiSelected
  }, props))
}


code:
export const FAVORITE_ANIMATION = [
  {
    properties: [
      { transform: 'scale(1)' },
      { transform: 'scale(2)' },
      { transform: 'scale(1)' }
    ],
    options: {
      duration: 333,
      easing: 'ease-in-out'
    }
  },
  {
    properties: [
      { fill: 'var(--action-button-fill-color)' },
      { fill: 'var(--action-button-fill-color-pressed)' }
    ],
    options: {
      duration: 333,
      easing: 'linear'
    }
  }
]

export const REBLOG_ANIMATION = FAVORITE_ANIMATION

export const FOLLOW_BUTTON_ANIMATION = [
  {
    properties: [
      { transform: 'scale(1)' },
      { transform: 'scale(2)' },
      { transform: 'scale(1)' }
    ],
    options: {
      duration: 333,
      easing: 'ease-in-out'
    }
  }
]


code: export const SEARCH_RESULTS_LIMIT = 4


code:
export const HOME_REBLOGS = 'homeReblogs'
export const HOME_REPLIES = 'homeReplies'

export const NOTIFICATION_REBLOGS = 'notificationReblogs'
export const NOTIFICATION_FAVORITES = 'notificationFavs'
export const NOTIFICATION_FOLLOWS = 'notificationFollows'
export const NOTIFICATION_MENTIONS = 'notificationMentions'
export const NOTIFICATION_POLLS = 'notificationPolls'


code:
export const DEFAULT_MEDIA_WIDTH = 300
export const DEFAULT_MEDIA_HEIGHT = 200

export const ONE_TRANSPARENT_PIXEL =
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='

export const mediaAccept = '.jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,.mov,image/jpeg,image/png,' +
  'image/gif,video/webm,video/mp4,video/quicktime,' +
  // some instances allow audio uploads
  'audio/mpeg,audio/mp4,audio/vnd.wav,audio/wav,audio/x-wav,audio/x-wave,audio/ogg'


code:
export const POLL_EXPIRY_OPTIONS = [
  {
    'value': 300,
    'label': '5 minutes'
  },
  {
    'value': 1800,
    'label': '30 minutes'
  },
  {
    'value': 3600,
    'label': '1 hour'
  },
  {
    'value': 21600,
    'label': '6 hours'
  },
  {
    'value': 86400,
    'label': '1 day'
  },
  {
    'value': 259200,
    'label': '3 days'
  },
  {
    'value': 604800,
    'label': '7 days'
  }
]

export const POLL_EXPIRY_DEFAULT = 86400


code:
export const POST_PRIVACY_OPTIONS = [
  {
    label: 'Public',
    key: 'public',
    icon: '#fa-globe'
  },
  {
    label: 'Unlisted',
    key: 'unlisted',
    icon: '#fa-unlock'
  },
  {
    label: 'Followers-only',
    key: 'private',
    icon: '#fa-lock'
  },
  {
    label: 'Direct',
    key: 'direct',
    icon: '#fa-envelope'
  }
]

export const LONG_POST_LENGTH = 1024
export const LONG_POST_TEXT = 'Long post'


code:
const themes = [
  {
    name: 'default',
    label: 'Royal',
    dark: false,
    color: 'royalblue'
  },
  {
    name: 'scarlet',
    label: 'Scarlet',
    dark: false,
    color: '#e04e41'
  },
  {
    name: 'seafoam',
    label: 'Seafoam',
    dark: false,
    color: '#177380'
  },
  {
    name: 'hotpants',
    label: 'Hotpants',
    dark: false,
    color: 'hotpink'
  },
  {
    name: 'oaken',
    label: 'Oaken',
    dark: false,
    color: 'saddlebrown'
  },
  {
    name: 'majesty',
    label: 'Majesty',
    dark: false,
    color: 'blueviolet'
  },
  {
    name: 'gecko',
    label: 'Gecko',
    dark: false,
    color: '#4ab92f'
  },
  {
    name: 'ozark',
    label: 'Ozark',
    dark: true,
    color: '#5263af'
  },
  {
    name: 'cobalt',
    label: 'Cobalt',
    dark: true,
    color: '#08439b'
  },
  {
    name: 'sorcery',
    label: 'Sorcery',
    dark: true,
    color: '#ae91e8'
  },
  {
    name: 'punk',
    label: 'Punk',
    dark: true,
    color: '#e04e41'
  },
  {
    name: 'riot',
    label: 'Riot',
    dark: true,
    color: 'hotpink'
  },
  {
    name: 'hacker',
    label: 'Hacker',
    dark: true,
    color: '#4ab92f'
  },
  {
    name: 'mastodon',
    label: 'Mastodon',
    dark: true,
    color: '#282C37'
  },
  {
    name: 'pitchblack',
    label: 'Pitch Black',
    dark: true,
    color: '#000'
  }
]

export { themes }


code:
export const TIMELINE_BATCH_SIZE = 20

export const timelines = {
  home: { name: 'home', label: 'Home' },
  local: { name: 'local', label: 'Local' },
  federated: { name: 'federated', label: 'Federated' }
}


code:
import { Store } from 'svelte/store'
import { safeLocalStorage as LS } from '../_utils/safeLocalStorage'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'

function safeParse (str) {
  return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str))
}

export class LocalStorageStore extends Store {
  constructor (state, keysToWatch) {
    super(state)
    if (!process.browser) {
      return
    }
    this._keysToWatch = keysToWatch
    this._keysToSave = {}
    let newState = {}
    for (let i = 0, len = LS.length; i < len; i++) {
      let key = LS.key(i)
      if (key.startsWith('store_')) {
        let item = LS.getItem(key)
        newState[key.substring(6)] = safeParse(item)
      }
    }
    this.set(newState)
    this.on('state', ({ changed }) => {
      Object.keys(changed).forEach(change => {
        if (this._keysToWatch.has(change)) {
          this._keysToSave[change] = true
        }
      })
    })
    if (process.browser) {
      lifecycle.addEventListener('statechange', e => {
        if (e.newState === 'passive') {
          console.log('saving LocalStorageStore...')
          this.save()
        }
      })
    }
  }

  save () {
    if (!process.browser) {
      return
    }
    Object.keys(this._keysToSave).forEach(key => {
      LS.setItem(`store_${key}`, JSON.stringify(this.get()[key]))
    })
    this._keysToSave = {}
  }
}


code:
import { get } from '../../_utils/lodash-lite'

const MIN_PREFIX_LENGTH = 2
// Technically mastodon accounts allow dots, but it would be weird to do an autosuggest search if it ends with a dot.
// Also this is rare. https://github.com/tootsuite/mastodon/pull/6844
const VALID_ACCOUNT_AND_EMOJI_CHAR = '\\w'
const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@${VALID_ACCOUNT_AND_EMOJI_CHAR}{${MIN_PREFIX_LENGTH},})$`)
const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:${VALID_ACCOUNT_AND_EMOJI_CHAR}{${MIN_PREFIX_LENGTH},})$`)

function computeForAutosuggest (store, key, defaultValue) {
  store.compute(key,
    ['currentInstance', 'currentComposeRealm', `autosuggestData_${key}`],
    (currentInstance, currentComposeRealm, root) => (
      get(root, [currentInstance, currentComposeRealm], defaultValue)
    )
  )
}

export function autosuggestComputations (store) {
  computeForAutosuggest(store, 'composeFocused', false)
  computeForAutosuggest(store, 'composeSelectionStart', 0)
  computeForAutosuggest(store, 'autosuggestSelected', 0)
  computeForAutosuggest(store, 'autosuggestSearchResults', [])
  computeForAutosuggest(store, 'autosuggestType', null)

  store.compute(
    'currentComposeText',
    ['currentComposeData', 'currentComposeRealm'],
    (currentComposeData, currentComposeRealm) => (
      get(currentComposeData, [currentComposeRealm, 'text'], '')
    )
  )

  store.compute(
    'autosuggestSearchText',
    ['currentComposeText', 'composeSelectionStart'],
    (currentComposeText, composeSelectionStart) => {
      let selectionStart = composeSelectionStart
      if (!currentComposeText || selectionStart < MIN_PREFIX_LENGTH) {
        return ''
      }

      let textUpToCursor = currentComposeText.substring(0, selectionStart)
      let match = textUpToCursor.match(ACCOUNT_SEARCH_REGEX) || textUpToCursor.match(EMOJI_SEARCH_REGEX)
      return (match && match[1]) || ''
    }
  )

  store.compute(
    'autosuggestNumSearchResults',
    ['autosuggestSearchResults'],
    (autosuggestSearchResults) => autosuggestSearchResults.length
  )

  store.compute(
    'autosuggestShown',
    ['composeFocused', 'autosuggestSearchText', 'autosuggestNumSearchResults'],
    (composeFocused, autosuggestSearchText, autosuggestNumSearchResults) => (
      !!(composeFocused && autosuggestSearchText && autosuggestNumSearchResults)
    )
  )
}


code:
import { instanceComputations } from './instanceComputations'
import { timelineComputations } from './timelineComputations'
import { navComputations } from './navComputations'
import { autosuggestComputations } from './autosuggestComputations'

export function computations (store) {
  instanceComputations(store)
  timelineComputations(store)
  navComputations(store)
  autosuggestComputations(store)
}


code:
import { DEFAULT_THEME } from '../../_utils/themeEngine'

function computeForInstance (store, computedKey, key, defaultValue) {
  store.compute(computedKey,
    [key, 'currentInstance'],
    (instanceData, currentInstance) => (currentInstance && instanceData[currentInstance]) || defaultValue)
}

export function instanceComputations (store) {
  computeForInstance(store, 'currentTheme', 'instanceThemes', DEFAULT_THEME)
  computeForInstance(store, 'currentVerifyCredentials', 'verifyCredentials', null)
  computeForInstance(store, 'currentInstanceInfo', 'instanceInfos', null)
  computeForInstance(store, 'pinnedPage', 'pinnedPages', '/local')
  computeForInstance(store, 'lists', 'instanceLists', [])
  computeForInstance(store, 'currentStatusModifications', 'statusModifications', null)
  computeForInstance(store, 'currentCustomEmoji', 'customEmoji', [])
  computeForInstance(store, 'currentComposeData', 'composeData', {})

  store.compute(
    'isUserLoggedIn',
    ['currentInstance', 'loggedInInstances'],
    (currentInstance, loggedInInstances) => !!(currentInstance && Object.keys(loggedInInstances).includes(currentInstance))
  )

  store.compute(
    'loggedInInstancesAsList',
    ['currentInstance', 'loggedInInstances', 'loggedInInstancesInOrder'],
    (currentInstance, loggedInInstances, loggedInInstancesInOrder) => {
      return loggedInInstancesInOrder.map(instanceName => {
        return Object.assign({
          current: currentInstance === instanceName,
          name: instanceName
        }, loggedInInstances[instanceName])
      })
    }
  )

  store.compute(
    'currentInstanceData',
    ['currentInstance', 'loggedInInstances'],
    (currentInstance, loggedInInstances) => {
      return Object.assign({
        name: currentInstance
      }, loggedInInstances[currentInstance])
    })

  store.compute(
    'accessToken',
    ['currentInstanceData'],
    (currentInstanceData) => currentInstanceData && currentInstanceData.access_token
  )

  store.compute(
    'maxStatusChars',
    ['currentInstanceInfo'],
    (currentInstanceInfo) => (
      // unofficial api used in glitch-soc and pleroma
      (currentInstanceInfo && currentInstanceInfo.max_toot_chars) || 500
    )
  )
}


code:
export function navComputations (store) {
  store.compute(
    'pinnedListTitle',
    ['lists', 'pinnedPage'],
    (lists, pinnedPage) => {
      if (!pinnedPage.startsWith('/lists')) {
        return
      }
      let listId = pinnedPage.split('/').slice(-1)[0]
      let list = lists.find(_ => _.id === listId)
      return list ? list.title : ''
    }
  )

  store.compute(
    'navPages',
    ['pinnedPage', 'pinnedListTitle'],
    (pinnedPage, pinnedListTitle) => {
      let pinnedPageObject
      if (pinnedPage === '/federated') {
        pinnedPageObject = {
          name: 'federated',
          href: '/federated',
          svg: '#fa-globe',
          label: 'Federated'
        }
      } else if (pinnedPage === '/direct') {
        pinnedPageObject = {
          name: 'direct',
          href: '/direct',
          svg: '#fa-envelope',
          label: 'Direct messages'
        }
      } else if (pinnedPage === '/favorites') {
        pinnedPageObject = {
          name: 'favorites',
          href: '/favorites',
          svg: '#fa-star',
          label: 'Favorites'
        }
      } else if (pinnedPage.startsWith('/lists/')) {
        pinnedPageObject = {
          name: `lists/${pinnedPage.split('/').slice(-1)[0]}`,
          href: pinnedPage,
          svg: '#fa-bars',
          label: pinnedListTitle
        }
      } else { // local
        pinnedPageObject = {
          name: 'local',
          href: '/local',
          svg: '#fa-users',
          label: 'Local'
        }
      }

      return [
        {
          name: 'home',
          href: '/',
          svg: '#pinafore-logo',
          label: 'Home'
        },
        {
          name: 'notifications',
          href: '/notifications',
          svg: '#fa-bell',
          label: 'Notifications'
        },
        pinnedPageObject,
        {
          name: 'community',
          href: '/community',
          svg: '#fa-comments',
          label: 'Community'
        },
        {
          name: 'search',
          href: '/search',
          svg: '#fa-search',
          label: 'Search'
        },
        {
          name: 'settings',
          href: '/settings',
          svg: '#fa-gear',
          label: 'Settings'
        }
      ]
    }
  )
}


code:
import { get } from '../../_utils/lodash-lite'
import { getFirstIdFromItemSummaries, getLastIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'
import {
  HOME_REBLOGS,
  HOME_REPLIES,
  NOTIFICATION_REBLOGS,
  NOTIFICATION_FOLLOWS,
  NOTIFICATION_FAVORITES,
  NOTIFICATION_POLLS,
  NOTIFICATION_MENTIONS
} from '../../_static/instanceSettings'

function computeForTimeline (store, key, defaultValue) {
  store.compute(key,
    ['currentInstance', 'currentTimeline', `timelineData_${key}`],
    (currentInstance, currentTimeline, root) => (
      get(root, [currentInstance, currentTimeline], defaultValue)
    )
  )
}

// Compute just the boolean, e.g. 'showPolls', so that we can use that boolean as
// the input to the timelineFilterFunction computations. This should reduce the need to
// re-compute the timelineFilterFunction over and over.
function computeTimelineFilter (store, computationName, timelinesToSettingsKeys) {
  store.compute(
    computationName,
    ['currentInstance', 'instanceSettings', 'currentTimeline'],
    (currentInstance, instanceSettings, currentTimeline) => {
      let settingsKey = timelinesToSettingsKeys[currentTimeline]
      return settingsKey ? get(instanceSettings, [currentInstance, settingsKey], true) : true
    }
  )
}

// Ditto for notifications, which we always have to keep track of due to the notification count.
function computeNotificationFilter (store, computationName, key) {
  store.compute(
    computationName,
    ['currentInstance', 'instanceSettings'],
    (currentInstance, instanceSettings) => {
      return get(instanceSettings, [currentInstance, key], true)
    }
  )
}

export function timelineComputations (store) {
  computeForTimeline(store, 'timelineItemSummaries', null)
  computeForTimeline(store, 'timelineItemSummariesToAdd', null)
  computeForTimeline(store, 'runningUpdate', false)
  computeForTimeline(store, 'lastFocusedElementId', null)
  computeForTimeline(store, 'ignoreBlurEvents', false)
  computeForTimeline(store, 'showHeader', false)
  computeForTimeline(store, 'shouldShowHeader', false)
  computeForTimeline(store, 'timelineItemSummariesAreStale', false)

  store.compute('currentTimelineType', ['currentTimeline'], currentTimeline => (
    currentTimeline && currentTimeline.split('/')[0])
  )
  store.compute('currentTimelineValue', ['currentTimeline'], currentTimeline => {
    if (!currentTimeline) {
      return void 0
    }
    let split = currentTimeline.split('/')
    let len = split.length
    if (split[len - 1] === 'with_replies' || split[len - 1] === 'media') {
      return split[len - 2]
    }
    return split[len - 1]
  })
  store.compute('firstTimelineItemId', ['timelineItemSummaries'], (timelineItemSummaries) => (
    getFirstIdFromItemSummaries(timelineItemSummaries)
  ))
  store.compute('lastTimelineItemId', ['timelineItemSummaries'], (timelineItemSummaries) => (
    getLastIdFromItemSummaries(timelineItemSummaries)
  ))

  computeTimelineFilter(store, 'timelineShowReblogs', { home: HOME_REBLOGS, notifications: NOTIFICATION_REBLOGS })
  computeTimelineFilter(store, 'timelineShowReplies', { home: HOME_REPLIES })
  computeTimelineFilter(store, 'timelineShowFollows', { notifications: NOTIFICATION_FOLLOWS })
  computeTimelineFilter(store, 'timelineShowFavs', { notifications: NOTIFICATION_FAVORITES })
  computeTimelineFilter(store, 'timelineShowMentions', { notifications: NOTIFICATION_MENTIONS })
  computeTimelineFilter(store, 'timelineShowPolls', { notifications: NOTIFICATION_POLLS })

  computeNotificationFilter(store, 'timelineNotificationShowReblogs', NOTIFICATION_REBLOGS)
  computeNotificationFilter(store, 'timelineNotificationShowFollows', NOTIFICATION_FOLLOWS)
  computeNotificationFilter(store, 'timelineNotificationShowFavs', NOTIFICATION_FAVORITES)
  computeNotificationFilter(store, 'timelineNotificationShowMentions', NOTIFICATION_MENTIONS)
  computeNotificationFilter(store, 'timelineNotificationShowPolls', NOTIFICATION_POLLS)

  function createFilterFunction (showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls) {
    return item => {
      switch (item.type) {
        case 'poll':
          return showPolls
        case 'favourite':
          return showFavs
        case 'reblog':
          return showReblogs
        case 'mention':
          return showMentions
        case 'follow':
          return showFollows
      }
      if (item.reblogId) {
        return showReblogs
      } else if (item.replyId) {
        return showReplies
      } else {
        return true
      }
    }
  }

  store.compute(
    'timelineFilterFunction',
    [
      'timelineShowReblogs', 'timelineShowReplies', 'timelineShowFollows',
      'timelineShowFavs', 'timelineShowMentions', 'timelineShowPolls'
    ],
    (showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls) => (
      createFilterFunction(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls)
    )
  )

  store.compute(
    'timelineNotificationFilterFunction',
    [
      'timelineNotificationShowReblogs', 'timelineNotificationShowFollows',
      'timelineNotificationShowFavs', 'timelineNotificationShowMentions',
      'timelineNotificationShowPolls'
    ],
    (showReblogs, showFollows, showFavs, showMentions, showPolls) => (
      createFilterFunction(showReblogs, true, showFollows, showFavs, showMentions, showPolls)
    )
  )

  store.compute(
    'filteredTimelineItemSummaries',
    ['timelineItemSummaries', 'timelineFilterFunction'],
    (timelineItemSummaries, timelineFilterFunction) => {
      return timelineItemSummaries && timelineItemSummaries.filter(timelineFilterFunction)
    }
  )

  store.compute(
    'filteredTimelineItemSummariesToAdd',
    ['timelineItemSummariesToAdd', 'timelineFilterFunction'],
    (timelineItemSummariesToAdd, timelineFilterFunction) => {
      return timelineItemSummariesToAdd && timelineItemSummariesToAdd.filter(timelineFilterFunction)
    }
  )

  store.compute('timelineNotificationItemSummaries',
    [`timelineData_timelineItemSummariesToAdd`, 'timelineFilterFunction', 'currentInstance'],
    (root, timelineFilterFunction, currentInstance) => (
      get(root, [currentInstance, 'notifications'])
    )
  )

  store.compute(
    'filteredTimelineNotificationItemSummaries',
    ['timelineNotificationItemSummaries', 'timelineNotificationFilterFunction'],
    (timelineNotificationItemSummaries, timelineNotificationFilterFunction) => (
      timelineNotificationItemSummaries && timelineNotificationItemSummaries.filter(timelineNotificationFilterFunction)
    )
  )

  store.compute('numberOfNotifications',
    ['filteredTimelineNotificationItemSummaries'],
    (filteredTimelineNotificationItemSummaries) => (
      filteredTimelineNotificationItemSummaries ? filteredTimelineNotificationItemSummaries.length : 0
    )
  )

  store.compute('hasNotifications',
    ['numberOfNotifications', 'currentPage'],
    (numberOfNotifications, currentPage) => currentPage !== 'notifications' && !!numberOfNotifications
  )
}


code:
import { get } from '../../_utils/lodash-lite'

export function autosuggestMixins (Store) {
  Store.prototype.setForAutosuggest = function (instanceName, realm, obj) {
    let valuesToSet = {}
    for (let key of Object.keys(obj)) {
      let rootKey = `autosuggestData_${key}`
      let root = this.get()[rootKey] || {}
      let instanceData = root[instanceName] = root[instanceName] || {}
      instanceData[realm] = obj[key]
      valuesToSet[rootKey] = root
    }

    this.set(valuesToSet)
  }

  Store.prototype.setForCurrentAutosuggest = function (obj) {
    let { currentInstance, currentComposeRealm } = this.get()
    this.setForAutosuggest(currentInstance, currentComposeRealm, obj)
  }

  Store.prototype.getForCurrentAutosuggest = function (key) {
    let { currentInstance, currentComposeRealm } = this.get()
    return get(this.get()[`autosuggestData_${key}`], [currentInstance, currentComposeRealm])
  }
}


code:
import { get } from '../../_utils/lodash-lite'

export function instanceMixins (Store) {
  Store.prototype.setComposeData = function (realm, obj) {
    let { composeData, currentInstance } = this.get()
    let instanceNameData = composeData[currentInstance] = composeData[currentInstance] || {}
    instanceNameData[realm] = Object.assign(instanceNameData[realm] || {}, obj)
    this.set({ composeData })
  }

  Store.prototype.getComposeData = function (realm, key) {
    let { composeData, currentInstance } = this.get()
    return composeData[currentInstance] &&
      composeData[currentInstance][realm] &&
      composeData[currentInstance][realm][key]
  }

  Store.prototype.clearComposeData = function (realm) {
    let { composeData, currentInstance } = this.get()
    if (composeData && composeData[currentInstance]) {
      delete composeData[currentInstance][realm]
    }
    this.set({ composeData })
  }

  Store.prototype.getInstanceSetting = function (instanceName, settingName, defaultValue) {
    let { instanceSettings } = this.get()
    return get(instanceSettings, [instanceName, settingName], defaultValue)
  }

  Store.prototype.setInstanceSetting = function (instanceName, settingName, value) {
    let { instanceSettings } = this.get()
    if (!instanceSettings[instanceName]) {
      instanceSettings[instanceName] = {}
    }
    instanceSettings[instanceName][settingName] = value
    this.set({ instanceSettings })
  }
}


code:
import { timelineMixins } from './timelineMixins'
import { instanceMixins } from './instanceMixins'
import { statusMixins } from './statusMixins'
import { autosuggestMixins } from './autosuggestMixins'

export function mixins (Store) {
  instanceMixins(Store)
  timelineMixins(Store)
  statusMixins(Store)
  autosuggestMixins(Store)
}


code:
function getStatusModifications (store, instanceName) {
  let { statusModifications } = store.get()
  statusModifications[instanceName] = statusModifications[instanceName] || {
    favorites: {},
    reblogs: {},
    pins: {}
  }
  return statusModifications
}

function setStatusModification (store, instanceName, statusId, key, value) {
  let statusModifications = getStatusModifications(store, instanceName)
  statusModifications[instanceName][key][statusId] = value
  store.set({ statusModifications })
}

export function statusMixins (Store) {
  Store.prototype.setStatusFavorited = function (instanceName, statusId, favorited) {
    setStatusModification(this, instanceName, statusId, 'favorites', favorited)
  }

  Store.prototype.setStatusReblogged = function (instanceName, statusId, reblogged) {
    setStatusModification(this, instanceName, statusId, 'reblogs', reblogged)
  }

  Store.prototype.setStatusPinned = function (instanceName, statusId, pinned) {
    setStatusModification(this, instanceName, statusId, 'pins', pinned)
  }
}


code:
import { pickBy, get } from '../../_utils/lodash-lite'

export function timelineMixins (Store) {
  Store.prototype.setForTimeline = function (instanceName, timelineName, obj) {
    let valuesToSet = {}
    for (let key of Object.keys(obj)) {
      let rootKey = `timelineData_${key}`
      let root = this.get()[rootKey] || {}
      let instanceData = root[instanceName] = root[instanceName] || {}
      instanceData[timelineName] = obj[key]
      valuesToSet[rootKey] = root
    }

    this.set(valuesToSet)
  }

  Store.prototype.getForTimeline = function (instanceName, timelineName, key) {
    let rootKey = `timelineData_${key}`
    let root = this.get()[rootKey]
    return get(root, [instanceName, timelineName])
  }

  Store.prototype.getAllTimelineData = function (instanceName, key) {
    let root = this.get()[`timelineData_${key}`] || {}
    return root[instanceName] || {}
  }

  Store.prototype.setForCurrentTimeline = function (obj) {
    let { currentInstance, currentTimeline } = this.get()
    this.setForTimeline(currentInstance, currentTimeline, obj)
  }

  Store.prototype.getThreads = function (instanceName) {
    let instanceData = this.getAllTimelineData(instanceName, 'timelineItemSummaries')

    return pickBy(instanceData, (value, key) => {
      return key.startsWith('status/')
    })
  }
}


code:
import { store } from '../store'
import { doEmojiSearch } from '../../_actions/autosuggestEmojiSearch'
import { doAccountSearch } from '../../_actions/autosuggestAccountSearch'

export function autosuggestObservers () {
  let lastSearch

  store.observe('autosuggestSearchText', async autosuggestSearchText => {
    // cancel any inflight XHRs or other operations
    if (lastSearch) {
      lastSearch.cancel()
      lastSearch = null
    }
    // autosuggestSelecting indicates that the user has pressed Enter or clicked on an item
    // and the results are being processed. Returning early avoids a flash of searched content.
    let { composeFocused } = store.get()
    let autosuggestSelecting = store.getForCurrentAutosuggest('autosuggestSelecting')
    if (!composeFocused || !autosuggestSearchText || autosuggestSelecting) {
      return
    }

    let autosuggestType = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji'

    if (autosuggestType === 'emoji') {
      lastSearch = doEmojiSearch(autosuggestSearchText)
    } else {
      lastSearch = doAccountSearch(autosuggestSearchText)
    }
  })
}


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

let theScrollbarStyle = process.browser && document.getElementById('theScrollbarStyle')

export function customScrollbarObservers () {
  store.observe('disableCustomScrollbars', disableCustomScrollbars => {
    if (!process.browser) {
      return
    }

    // disables or enables the style
    theScrollbarStyle.setAttribute('media', disableCustomScrollbars ? 'only x' : 'all')
  }, { init: false })
}


code:
import { updateInstanceInfo, updateVerifyCredentialsForInstance } from '../../_actions/instances'
import { updateListsForInstance } from '../../_actions/lists'
import { createStream } from '../../_actions/streaming'
import { updatePushSubscriptionForInstance } from '../../_actions/pushSubscription'
import { updateCustomEmojiForInstance } from '../../_actions/emoji'
import { addStatusesOrNotifications } from '../../_actions/addStatusOrNotification'
import { getTimeline } from '../../_api/timelines'
import { TIMELINE_BATCH_SIZE } from '../../_static/timelines'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import { mark, stop } from '../../_utils/marks'
import { store } from '../store'
import { getFirstIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'

// stream to watch for home timeline updates and notifications
let currentInstanceStream

async function refreshInstanceDataAndStream (store, instanceName) {
  mark(`refreshInstanceDataAndStream-${instanceName}`)
  await doRefreshInstanceDataAndStream(store, instanceName)
  stop(`refreshInstanceDataAndStream-${instanceName}`)
}

function currentInstanceChanged (store, instanceName) {
  return store.get().currentInstance !== instanceName
}

async function doRefreshInstanceDataAndStream (store, instanceName) {
  if (currentInstanceChanged(store, instanceName)) {
    return
  }

  await refreshInstanceData(instanceName)

  if (currentInstanceChanged(store, instanceName)) {
    return
  }

  let { currentInstanceInfo } = store.get()
  if (!currentInstanceInfo) {
    return
  }

  stream(store, instanceName, currentInstanceInfo)
}

async function refreshInstanceData (instanceName) {
  // these are all low-priority
  scheduleIdleTask(() => updateCustomEmojiForInstance(instanceName))
  scheduleIdleTask(() => updateListsForInstance(instanceName))
  scheduleIdleTask(() => updatePushSubscriptionForInstance(instanceName))

  // these are the only critical ones
  await Promise.all([
    updateInstanceInfo(instanceName),
    updateVerifyCredentialsForInstance(instanceName)
  ])
}

function stream (store, instanceName, currentInstanceInfo) {
  let homeTimelineItemSummaries = store.getForTimeline(instanceName,
    'home', 'timelineItemSummaries')
  let firstHomeTimelineItemId = getFirstIdFromItemSummaries(homeTimelineItemSummaries)
  let notificationItemSummaries = store.getForTimeline(instanceName,
    'notifications', 'timelineItemSummaries')
  let firstNotificationTimelineItemId = getFirstIdFromItemSummaries(notificationItemSummaries)

  let { accessToken } = store.get()
  let streamingApi = currentInstanceInfo.urls.streaming_api

  function onOpenStream () {
    if (currentInstanceChanged(store, instanceName)) {
      return
    }

    /* no await */ fillGap(instanceName, accessToken, 'home', firstHomeTimelineItemId)
    /* no await */ fillGap(instanceName, accessToken, 'notifications', firstNotificationTimelineItemId)
  }

  currentInstanceStream = createStream(streamingApi, instanceName, accessToken, 'home', onOpenStream)

  if (process.env.NODE_ENV !== 'production') {
    window.currentInstanceStream = currentInstanceStream
  }
}

// fill in the "streaming gap" – i.e. fetch the most recent 20 items so that there isn't
// a big gap in the timeline if you haven't looked at it in awhile
async function fillGap (instanceName, accessToken, timelineName, firstTimelineItemId) {
  if (!firstTimelineItemId) {
    return
  }
  let newTimelineItems = await getTimeline(instanceName, accessToken,
    timelineName, null, firstTimelineItemId, TIMELINE_BATCH_SIZE)
  if (newTimelineItems.length) {
    addStatusesOrNotifications(instanceName, timelineName, newTimelineItems)
  }
}

export function instanceObservers () {
  store.observe('currentInstance', async (currentInstance) => {
    if (!process.browser) {
      return
    }
    if (currentInstanceStream) {
      currentInstanceStream.close()
      currentInstanceStream = null
      if (process.env.NODE_ENV !== 'production') {
        window.currentInstanceStream = null
      }
    }
    if (!currentInstance) {
      return
    }

    scheduleIdleTask(() => refreshInstanceDataAndStream(store, currentInstance))
  })
}


code:
import { onUserIsLoggedOut } from '../../_actions/onUserIsLoggedOut'

export function logOutObservers (store) {
  if (!process.browser) {
    return
  }
  store.observe('isUserLoggedIn', isUserLoggedIn => {
    if (!isUserLoggedIn) {
      onUserIsLoggedOut()
    }
  })
}


code:
import { instanceObservers } from './instanceObservers'
import { timelineObservers } from './timelineObservers'
import { notificationObservers } from './notificationObservers'
import { autosuggestObservers } from './autosuggestObservers'
import { notificationPermissionObservers } from './notificationPermissionObservers'
import { customScrollbarObservers } from './customScrollbarObservers'

// These observers can be lazy-loaded when the user is actually logged in.
// Prevents circular dependencies and reduces the size of main.js
export default function loggedInObservers () {
  instanceObservers()
  timelineObservers()
  notificationObservers()
  autosuggestObservers()
  notificationPermissionObservers()
  customScrollbarObservers()
}


code:
import { emit } from '../../_utils/eventBus'

export function navObservers (store) {
  function pageIsInNav (store, page) {
    let { navPages } = store.get()
    return !!navPages.find(_ => _.name === page)
  }

  store.observe('currentPage', (currentPage, previousPage) => {
    if (currentPage && previousPage &&
        pageIsInNav(store, currentPage) &&
        pageIsInNav(store, previousPage)) {
      emit('animateNavPart1', {
        fromPage: previousPage,
        toPage: currentPage
      })
    }
  }, {
    init: false
  })
}


code:
import { setFavicon } from '../../_utils/setFavicon'
import { runMediumPriorityTask } from '../../_utils/runMediumPriorityTask'
import { store } from '../store'

let currentFaviconHasNotifications = false

export function notificationObservers () {
  store.observe('hasNotifications', hasNotifications => {
    if (!process.browser) {
      return
    }
    runMediumPriorityTask(() => {
      if (currentFaviconHasNotifications === hasNotifications) {
        return
      }
      setFavicon(hasNotifications ? '/favicon-alert.png' : '/favicon.png')
      currentFaviconHasNotifications = !currentFaviconHasNotifications
    })
  })
}


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

export function notificationPermissionObservers () {
  if (!process.browser || !navigator.permissions || !navigator.permissions.query) {
    return
  }

  navigator.permissions.query({ name: 'notifications' }).then(permission => {
    store.set({ notificationPermission: permission.state })

    permission.onchange = event => {
      store.set({ notificationPermission: event.target.state })
    }
  })
}


code:
// For convenience, periodically re-compute the current time. This ensures freshness of
// displays like "x minutes ago" without having to jump through a lot of hoops.
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'

const POLL_INTERVAL = 10000

export function nowObservers (store) {
  let interval

  function updateNow () {
    store.set({ now: Date.now() })
  }

  function startPolling () {
    interval = setInterval(() => scheduleIdleTask(updateNow), POLL_INTERVAL)
  }

  function stopPolling () {
    if (interval) {
      clearInterval(interval)
      interval = null
    }
  }

  function restartPolling () {
    stopPolling()
    scheduleIdleTask(updateNow)
    startPolling()
  }

  updateNow()

  if (process.browser) {
    startPolling()

    lifecycle.addEventListener('statechange', e => {
      if (e.newState === 'passive') {
        console.log('stopping Date.now() observer...')
        stopPolling()
      } else if (e.newState === 'active') {
        console.log('restarting Date.now() observer...')
        restartPolling()
      }
    })
  }
}


code:
import { onlineObservers } from './onlineObservers'
import { nowObservers } from './nowObservers'
import { navObservers } from './navObservers'
import { pageVisibilityObservers } from './pageVisibilityObservers'
import { resizeObservers } from './resizeObservers'
import { setupLoggedInObservers } from './setupLoggedInObservers'
import { logOutObservers } from './logOutObservers'
import { touchObservers } from './touchObservers'

export function observers (store) {
  onlineObservers(store)
  nowObservers(store)
  navObservers(store)
  pageVisibilityObservers(store)
  resizeObservers(store)
  touchObservers(store)
  logOutObservers(store)
  setupLoggedInObservers(store)
}


code:
import debounce from 'lodash-es/debounce'
import { toast } from '../../_components/toast/toast'

const OFFLINE_DELAY = 5000
const NOTIFY_OFFLINE_LIMIT = 1

let notifyCount = 0

let offlineStyle = process.browser && document.getElementById('theOfflineStyle')

// debounce to avoid notifying for a short connection issue
const notifyOffline = debounce(() => {
  if (process.browser && !navigator.onLine && ++notifyCount <= NOTIFY_OFFLINE_LIMIT) {
    toast.say('You seem to be offline. You can still read toots while offline.')
  }
}, OFFLINE_DELAY)

export function onlineObservers (store) {
  if (!process.browser) {
    return
  }
  let meta = document.getElementById('theThemeColor')
  let oldTheme = meta.content

  store.observe('online', online => {
    // "only x" ensures the <style> tag does not have any effect
    offlineStyle.setAttribute('media', online ? 'only x' : 'all')
    if (online) {
      meta.content = oldTheme
    } else {
      let offlineThemeColor = window.__themeColors.offline
      if (meta.content !== offlineThemeColor) {
        oldTheme = meta.content
      }
      meta.content = offlineThemeColor
      notifyOffline()
    }
  })

  window.addEventListener('offline', () => store.set({ online: false }))
  window.addEventListener('online', () => store.set({ online: true }))
}


code:
export function pageVisibilityObservers (store) {
  if (!process.browser) {
    return
  }

  document.addEventListener('visibilitychange', () => {
    store.set({ pageVisibilityHidden: document.hidden })
  })
}


code:
import { registerResizeListener } from '../../_utils/resize'

export function resizeObservers (store) {
  if (!process.browser) {
    return
  }

  const recalculateIsMobileSize = () => {
    store.set({
      isMobileSize: window.matchMedia('(max-width: 767px)').matches, //  e.g. iPhone Plus
      isSmallMobileSize: window.matchMedia('(max-width: 479px)').matches, // e.g. Galaxy S5
      isTinyMobileSize: window.matchMedia('(max-width: 320px)').matches // e.g. iPhone 4
    })
  }

  registerResizeListener(recalculateIsMobileSize)
  recalculateIsMobileSize()
}


code:
import { importLoggedInObservers } from '../../_utils/asyncModules'

let observedOnce = false

// An observer that calls an observer... this is a bit weird, but it eliminates
// circular dependencies and also allows us to lazy load observers that are
// only needed when you're logged in.
export function setupLoggedInObservers (store) {
  store.observe('isUserLoggedIn', isUserLoggedIn => {
    if (isUserLoggedIn && !observedOnce) {
      importLoggedInObservers().then(loggedInObservers => loggedInObservers())
      observedOnce = true
    }
  })
}


code:
import { updateInstanceInfo } from '../../_actions/instances'
import { createStream } from '../../_actions/streaming'
import { getTimeline } from '../../_api/timelines'
import { addStatusesOrNotifications } from '../../_actions/addStatusOrNotification'
import { TIMELINE_BATCH_SIZE } from '../../_static/timelines'
import { store } from '../store'
import { getFirstIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'

export function timelineObservers () {
  // stream to watch for local/federated/etc. updates. home and notification
  // updates are handled in timelineObservers.js
  let currentTimelineStream

  function shutdownPreviousStream () {
    if (currentTimelineStream) {
      currentTimelineStream.close()
      currentTimelineStream = null
      if (process.env.NODE_ENV !== 'production') {
        window.currentTimelineStream = null
      }
    }
  }

  function shouldObserveTimeline (timeline) {
    return timeline &&
      !(
        timeline !== 'local' &&
        timeline !== 'federated' &&
        timeline !== 'direct' &&
        !timeline.startsWith('list/') &&
        !timeline.startsWith('tag/')
      )
  }

  store.observe('currentTimeline', async (currentTimeline) => {
    if (!process.browser) {
      return
    }

    shutdownPreviousStream()

    if (!shouldObserveTimeline(currentTimeline)) {
      return
    }

    let { currentInstance } = store.get()
    let { accessToken } = store.get()
    await updateInstanceInfo(currentInstance)

    let currentTimelineIsUnchanged = () => {
      let {
        currentInstance: newCurrentInstance,
        currentTimeline: newCurrentTimeline
      } = store.get()
      return newCurrentInstance === currentInstance &&
        newCurrentTimeline === currentTimeline
    }

    if (!currentTimelineIsUnchanged()) {
      return
    }

    let timelineItemSummaries = store.getForTimeline(currentInstance,
      currentTimeline, 'timelineItemSummaries')
    let firstTimelineItemId = getFirstIdFromItemSummaries(timelineItemSummaries)

    let onOpenStream = async () => {
      if (!firstTimelineItemId || !currentTimelineIsUnchanged()) {
        return
      }
      // fill in the "streaming gap" – i.e. fetch the most recent 20 items so that there isn't
      // a big gap in the timeline if you haven't looked at it in awhile
      let newTimelineItems = await getTimeline(currentInstance, accessToken,
        currentTimeline, null, firstTimelineItemId, TIMELINE_BATCH_SIZE)
      if (newTimelineItems.length) {
        addStatusesOrNotifications(currentInstance, currentTimeline, newTimelineItems)
      }
    }

    let { currentInstanceInfo } = store.get()
    let streamingApi = currentInstanceInfo.urls.streaming_api
    currentTimelineStream = createStream(streamingApi, currentInstance, accessToken,
      currentTimeline, onOpenStream)

    if (process.env.NODE_ENV !== 'production') {
      window.currentTimelineStream = currentTimelineStream
    }
  })
}


code:
export function touchObservers (store) {
  if (!process.browser) {
    return
  }

  let onTouch = () => {
    store.set({ isUserTouching: true })
    window.removeEventListener('touchstart', onTouch)
  }

  window.addEventListener('touchstart', onTouch, { passive: true })
}


code:
import { observers } from './observers/observers'
import { computations } from './computations/computations'
import { mixins } from './mixins/mixins'
import { LocalStorageStore } from './LocalStorageStore'
import { observe } from 'svelte-extras'

const persistedState = {
  autoplayGifs: false,
  composeData: {},
  currentInstance: null,
  currentRegisteredInstanceName: undefined,
  currentRegisteredInstance: undefined,
  // we disable scrollbars by default on iOS
  disableCustomScrollbars: process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent),
  disableHotkeys: false,
  disableInfiniteScroll: false,
  disableLongAriaLabels: false,
  disableTapOnStatus: false,
  hideCards: false,
  largeInlineMedia: false,
  instanceNameInSearch: '',
  instanceThemes: {},
  instanceSettings: {},
  loggedInInstances: {},
  loggedInInstancesInOrder: [],
  markMediaAsSensitive: false,
  neverMarkMediaAsSensitive: false,
  omitEmojiInDisplayNames: undefined,
  pinnedPages: {},
  pushSubscription: null,
  reduceMotion:
    !process.browser ||
    window.matchMedia('(prefers-reduced-motion: reduce)').matches,
  underlineLinks: false
}

const nonPersistedState = {
  customEmoji: {},
  instanceInfos: {},
  instanceLists: {},
  online: !process.browser || navigator.onLine,
  pinnedStatuses: {},
  polls: {},
  pushNotificationsSupport:
    process.browser &&
    ('serviceWorker' in navigator &&
      'PushManager' in window &&
      'getKey' in window.PushSubscription.prototype),
  queryInSearch: '',
  repliesShown: {},
  sensitivesShown: {},
  spoilersShown: {},
  statusModifications: {},
  verifyCredentials: {},
  openShareDialog: false
}

const state = Object.assign({}, persistedState, nonPersistedState)
const keysToStoreInLocalStorage = new Set(Object.keys(persistedState))

class PinaforeStore extends LocalStorageStore {
  constructor (state) {
    super(state, keysToStoreInLocalStorage)
  }
}

PinaforeStore.prototype.observe = observe

export const store = new PinaforeStore(state)

mixins(PinaforeStore)
computations(store)
observers(store)

if (process.browser && process.env.NODE_ENV !== 'production') {
  window.store = store // for debugging
}

// needed for tests
if (process.browser) {
  window.__forceOnline = online => store.set({ online })
}


code:
// Forked from a11y-dialog 4.0.1, adding a small change to element.focus() to work
// around a Chrome bug with sticky positioning (https://github.com/nolanlawson/pinafore/issues/671)
// Original: https://unpkg.com/a11y-dialog@4.0.1/a11y-dialog.js

var FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])']
var TAB_KEY = 9
var ESCAPE_KEY = 27
var focusedBeforeDialog

/**
   * Define the constructor to instantiate a dialog
   *
   * @constructor
   * @param {Element} node
   * @param {(NodeList | Element | string)} targets
   */
function A11yDialog (node, targets) {
  // Prebind the functions that will be bound in addEventListener and
  // removeEventListener to avoid losing references
  this._show = this.show.bind(this)
  this._hide = this.hide.bind(this)
  this._maintainFocus = this._maintainFocus.bind(this)
  this._bindKeypress = this._bindKeypress.bind(this)

  // Keep a reference of the node on the instance
  this.node = node

  // Keep an object of listener types mapped to callback functions
  this._listeners = {}

  // Initialise everything needed for the dialog to work properly
  this.create(targets)
}

/**
   * Set up everything necessary for the dialog to be functioning
   *
   * @param {(NodeList | Element | string)} targets
   * @return {this}
   */
A11yDialog.prototype.create = function (targets) {
  // Keep a collection of nodes to disable/enable when toggling the dialog
  this._targets = this._targets || collect(targets) || getSiblings(this.node)

  // Make sure the dialog element is disabled on load, and that the `shown`
  // property is synced with its value
  this.node.setAttribute('aria-hidden', true)
  this.shown = false

  // Keep a collection of dialog openers, each of which will be bound a click
  // event listener to open the dialog
  this._openers = $$('[data-a11y-dialog-show="' + this.node.id + '"]')
  this._openers.forEach(function (opener) {
    opener.addEventListener('click', this._show)
  }.bind(this))

  // Keep a collection of dialog closers, each of which will be bound a click
  // event listener to close the dialog
  this._closers = $$('[data-a11y-dialog-hide]', this.node)
    .concat($$('[data-a11y-dialog-hide="' + this.node.id + '"]'))
  this._closers.forEach(function (closer) {
    closer.addEventListener('click', this._hide)
  }.bind(this))

  // Execute all callbacks registered for the `create` event
  this._fire('create')

  return this
}

/**
   * Show the dialog element, disable all the targets (siblings), trap the
   * current focus within it, listen for some specific key presses and fire all
   * registered callbacks for `show` event
   *
   * @param {Event} event
   * @return {this}
   */
A11yDialog.prototype.show = function (event) {
  // If the dialog is already open, abort
  if (this.shown) {
    return this
  }

  this.shown = true
  this.node.removeAttribute('aria-hidden')

  // Iterate over the targets to disable them by setting their `aria-hidden`
  // attribute to `true`; in case they already have this attribute, keep a
  // reference of their original value to be able to restore it later
  this._targets.forEach(function (target) {
    var original = target.getAttribute('aria-hidden')

    if (original) {
      target.setAttribute('data-a11y-dialog-original', original)
    }

    target.setAttribute('aria-hidden', 'true')
  })

  // Keep a reference to the currently focused element to be able to restore
  // it later, then set the focus to the first focusable child of the dialog
  // element
  focusedBeforeDialog = document.activeElement
  setFocusToFirstItem(this.node)

  // Bind a focus event listener to the body element to make sure the focus
  // stays trapped inside the dialog while open, and start listening for some
  // specific key presses (TAB and ESC)
  document.body.addEventListener('focus', this._maintainFocus, true)
  document.addEventListener('keydown', this._bindKeypress)

  // Execute all callbacks registered for the `show` event
  this._fire('show', event)

  return this
}

/**
   * Hide the dialog element, enable all the targets (siblings), restore the
   * focus to the previously active element, stop listening for some specific
   * key presses and fire all registered callbacks for `hide` event
   *
   * @param {Event} event
   * @return {this}
   */
A11yDialog.prototype.hide = function (event) {
  // If the dialog is already closed, abort
  if (!this.shown) {
    return this
  }

  this.shown = false
  this.node.setAttribute('aria-hidden', 'true')

  // Iterate over the targets to enable them by remove their `aria-hidden`
  // attribute or resetting them to their initial value
  this._targets.forEach(function (target) {
    var original = target.getAttribute('data-a11y-dialog-original')

    if (original) {
      target.setAttribute('aria-hidden', original)
      target.removeAttribute('data-a11y-dialog-original')
    } else {
      target.removeAttribute('aria-hidden')
    }
  })

  // If their was a focused element before the dialog was opened, restore the
  // focus back to it
  if (focusedBeforeDialog) {
    // This double rAF is to work around a bug in Chrome when focusing sticky-positioned
    // elements. See https://github.com/nolanlawson/pinafore/issues/671
    requestAnimationFrame(() => requestAnimationFrame(() => focusedBeforeDialog.focus()))
  }

  // Remove the focus event listener to the body element and stop listening
  // for specific key presses
  document.body.removeEventListener('focus', this._maintainFocus, true)
  document.removeEventListener('keydown', this._bindKeypress)

  // Execute all callbacks registered for the `hide` event
  this._fire('hide', event)

  return this
}

/**
   * Destroy the current instance (after making sure the dialog has been hidden)
   * and remove all associated listeners from dialog openers and closers
   *
   * @return {this}
   */
A11yDialog.prototype.destroy = function () {
  // Hide the dialog to avoid destroying an open instance
  this.hide()

  // Remove the click event listener from all dialog openers
  this._openers.forEach(function (opener) {
    opener.removeEventListener('click', this._show)
  }.bind(this))

  // Remove the click event listener from all dialog closers
  this._closers.forEach(function (closer) {
    closer.removeEventListener('click', this._hide)
  }.bind(this))

  // Execute all callbacks registered for the `destroy` event
  this._fire('destroy')

  // Keep an object of listener types mapped to callback functions
  this._listeners = {}

  return this
}

/**
   * Register a new callback for the given event type
   *
   * @param {string} type
   * @param {Function} handler
   */
A11yDialog.prototype.on = function (type, handler) {
  if (typeof this._listeners[type] === 'undefined') {
    this._listeners[type] = []
  }

  this._listeners[type].push(handler)

  return this
}

/**
   * Unregister an existing callback for the given event type
   *
   * @param {string} type
   * @param {Function} handler
   */
A11yDialog.prototype.off = function (type, handler) {
  var index = this._listeners[type].indexOf(handler)

  if (index > -1) {
    this._listeners[type].splice(index, 1)
  }

  return this
}

/**
   * Iterate over all registered handlers for given type and call them all with
   * the dialog element as first argument, event as second argument (if any).
   *
   * @access private
   * @param {string} type
   * @param {Event} event
   */
A11yDialog.prototype._fire = function (type, event) {
  var listeners = this._listeners[type] || []

  listeners.forEach(function (listener) {
    listener(this.node, event)
  }.bind(this))
}

/**
   * Private event handler used when listening to some specific key presses
   * (namely ESCAPE and TAB)
   *
   * @access private
   * @param {Event} event
   */
A11yDialog.prototype._bindKeypress = function (event) {
  // If the dialog is shown and the ESCAPE key is being pressed, prevent any
  // further effects from the ESCAPE key and hide the dialog
  if (this.shown && event.which === ESCAPE_KEY) {
    event.preventDefault()
    this.hide()
  }

  // If the dialog is shown and the TAB key is being pressed, make sure the
  // focus stays trapped within the dialog element
  if (this.shown && event.which === TAB_KEY) {
    trapTabKey(this.node, event)
  }
}

/**
   * Private event handler used when making sure the focus stays within the
   * currently open dialog
   *
   * @access private
   * @param {Event} event
   */
A11yDialog.prototype._maintainFocus = function (event) {
  // If the dialog is shown and the focus is not within the dialog element,
  // move it back to its first focusable child
  if (this.shown && !this.node.contains(event.target)) {
    setFocusToFirstItem(this.node)
  }
}

/**
   * Convert a NodeList into an array
   *
   * @param {NodeList} collection
   * @return {Array<Element>}
   */
function toArray (collection) {
  return Array.prototype.slice.call(collection)
}

/**
   * Query the DOM for nodes matching the given selector, scoped to context (or
   * the whole document)
   *
   * @param {String} selector
   * @param {Element} [context = document]
   * @return {Array<Element>}
   */
function $$ (selector, context) {
  return toArray((context || document).querySelectorAll(selector))
}

/**
   * Return an array of Element based on given argument (NodeList, Element or
   * string representing a selector)
   *
   * @param {(NodeList | Element | string)} target
   * @return {Array<Element>}
   */
function collect (target) {
  if (NodeList.prototype.isPrototypeOf(target)) {
    return toArray(target)
  }

  if (Element.prototype.isPrototypeOf(target)) {
    return [target]
  }

  if (typeof target === 'string') {
    return $$(target)
  }
}

/**
   * Set the focus to the first focusable child of the given element
   *
   * @param {Element} node
   */
function setFocusToFirstItem (node) {
  var focusableChildren = getFocusableChildren(node)

  if (focusableChildren.length) {
    focusableChildren[0].focus()
  }
}

/**
   * Get the focusable children of the given element
   *
   * @param {Element} node
   * @return {Array<Element>}
   */
function getFocusableChildren (node) {
  return $$(FOCUSABLE_ELEMENTS.join(','), node).filter(function (child) {
    return !!(child.offsetWidth || child.offsetHeight || child.getClientRects().length)
  })
}

/**
   * Trap the focus inside the given element
   *
   * @param {Element} node
   * @param {Event} event
   */
function trapTabKey (node, event) {
  var focusableChildren = getFocusableChildren(node)
  var focusedItemIndex = focusableChildren.indexOf(document.activeElement)

  // If the SHIFT key is being pressed while tabbing (moving backwards) and
  // the currently focused item is the first one, move the focus to the last
  // focusable item from the dialog element
  if (event.shiftKey && focusedItemIndex === 0) {
    focusableChildren[focusableChildren.length - 1].focus()
    event.preventDefault()
    // If the SHIFT key is not being pressed (moving forwards) and the currently
    // focused item is the last one, move the focus to the first focusable item
    // from the dialog element
  } else if (!event.shiftKey && focusedItemIndex === focusableChildren.length - 1) {
    focusableChildren[0].focus()
    event.preventDefault()
  }
}

/**
   * Retrieve siblings from given element
   *
   * @param {Element} node
   * @return {Array<Element>}
   */
function getSiblings (node) {
  var nodes = toArray(node.parentNode.childNodes)
  var siblings = nodes.filter(function (node) {
    return node.nodeType === 1
  })

  siblings.splice(siblings.indexOf(node), 1)

  return siblings
}

export { A11yDialog }