code:
<form class="instance-actions" aria-label="Switch to or log out of this instance">
  {#if $loggedInInstancesInOrder.length > 1 && $currentInstance !== instanceName}
    <button class="primary"
            on:click="onSwitchToThisInstance(event)">
      Switch to this instance
    </button>
  {/if}
  <button on:click="onLogOut(event)">Log out</button>
</form>
<style>
  .instance-actions {
    width: 100%;
    display: flex;
    justify-content: right;
    margin: 20px 0;
  }
  .instance-actions button {
    margin: 0 5px;
    flex-basis: 100%;
  }
</style>
<script>
  import { store } from '../../../_store/store'
  import { importShowTextConfirmationDialog } from '../../dialog/asyncDialogs'
  import { switchToInstance, logOutOfInstance } from '../../../_actions/instances'

  export default {
    store: () => store,
    methods: {
      onSwitchToThisInstance (e) {
        e.preventDefault()
        let { instanceName } = this.get()
        switchToInstance(instanceName)
      },
      async onLogOut (e) {
        e.preventDefault()
        let { instanceName } = this.get()

        let showTextConfirmationDialog = await importShowTextConfirmationDialog()
        showTextConfirmationDialog({
          text: `Log out of ${instanceName}?`
        }).on('positive', () => {
          // TODO: dumb timing hack because the modal navigates back while we're trying to navigate forward
          setTimeout(() => {
            /* no await */logOutOfInstance(instanceName)
          }, 200)
        })
      }
    }
  }
</script>


code:
    <Avatar account={verifyCredentials} size="big" />

  <ExternalLink className="acct-handle"
                href={verifyCredentials.url} >
    {'@' + verifyCredentials.acct}
  </ExternalLink>

    <AccountDisplayName account={verifyCredentials} />

repo_name: RayoGundead/pinny

license: mit


code:
<GenericInstanceSettings
  {instanceName}
  {options}
  label="Notification filter settings"
/>
<script>
  import GenericInstanceSettings from './GenericInstanceSettings.html'
  import {
    NOTIFICATION_REBLOGS,
    NOTIFICATION_FAVORITES,
    NOTIFICATION_FOLLOWS,
    NOTIFICATION_MENTIONS,
    NOTIFICATION_POLLS
  } from '../../../_static/instanceSettings'

  export default {
    data: () => ({
      options: [
        {
          key: NOTIFICATION_FOLLOWS,
          label: 'New followers',
          defaultValue: true
        },
        {
          key: NOTIFICATION_FAVORITES,
          label: 'Favorites',
          defaultValue: true
        },
        {
          key: NOTIFICATION_REBLOGS,
          label: 'Boosts',
          defaultValue: true
        },
        {
          key: NOTIFICATION_MENTIONS,
          label: 'Mentions',
          defaultValue: true
        },
        {
          key: NOTIFICATION_POLLS,
          label: 'Poll results',
          defaultValue: true
        }
      ]
    }),
    components: {
      GenericInstanceSettings
    }
  }
</script>


code:
  {#if pushNotificationsSupport === false}

Your browser doesn't support push notifications.

  {:elseif $notificationPermission === "denied"}

You have denied permission to show notifications.

  {/if}
  <form id="push-notification-settings"
        disabled="{!pushNotificationsSupport}"
        ref:form
        aria-label="Push notification settings">
    {#each options as option, i (option.key)}
      {#if i > 0}

      {/if}
      <input type="checkbox"
             id="push-notifications-{option.key}"
             name="{option.key}"
             disabled="{!pushNotificationsSupport}"
             on:change="onPushSettingsChange(event)">
      {option.label}
    {/each}
  </form>


code:
<form class="theme-chooser" aria-label="Choose a theme">

    {#each themeGroups as themeGroup}

        {themeGroup.dark ? 'Dark background' : 'Light background' }

      {#each themeGroup.themes as theme}

        <input type="radio" id="choice-theme-{theme.name}"
               value={theme.name} checked="$currentTheme === theme.name"
               bind:group="selectedTheme" on:change="onThemeChange()">

            {theme.label} {theme.name === DEFAULT_THEME ? '(default)' : ''}

      {/each}

    {/each}

</form>


code:
<script>
  import {
    isVisible,
    firstVisibleElementIndex,
    scrollIntoViewIfNeeded } from '../../_utils/scrollIntoView'
  import {
    addShortcutFallback,
    removeShortcutFallback,
    onKeyDownInShortcutScope } from '../../_utils/shortcuts'
  import { smoothScroll } from '../../_utils/smoothScroll'
  import { getScrollContainer } from '../../_utils/scrollContainer'

  const VISIBILITY_CHECK_DELAY_MS = 600

  const keyToElement = key => document.getElementById(key)
  const elementToKey = element => element.getAttribute('id')
  const scope = 'global'

  export default {
    data: () => ({
      activeItemChangeTime: 0,
      elements: document.getElementsByClassName('shortcut-list-item')
    }),
    oncreate () {
      addShortcutFallback(scope, this)
    },
    ondestroy () {
      removeShortcutFallback(scope, this)
    },
    methods: {
      onKeyDown (event) {
        if (event.key === 'j' || event.key === 'ArrowDown') {
          event.stopPropagation()
          event.preventDefault()
          this.changeActiveItem(1, event.timeStamp)
          return
        }
        if (event.key === 'k' || event.key === 'ArrowUp') {
          event.stopPropagation()
          event.preventDefault()
          this.changeActiveItem(-1, event.timeStamp)
          return
        }
        let activeItemKey = this.checkActiveItem(event.timeStamp)
        if (!activeItemKey) {
          let { elements } = this.get()
          let index = firstVisibleElementIndex(elements).first
          if (index >= 0) {
            activeItemKey = elementToKey(elements[index])
          }
        }
        if (activeItemKey) {
          onKeyDownInShortcutScope(activeItemKey, event)
        }
      },
      changeActiveItem (movement, timeStamp) {
        let { elements } = this.get()
        let index = -1
        let activeItemKey = this.checkActiveItem(timeStamp)
        if (activeItemKey) {
          let len = elements.length
          let i = -1
          while (++i < len) {
            if (elementToKey(elements[i]) === activeItemKey) {
              index = i
              break
            }
          }
        }
        if (index === 0 && movement === -1) {
          activeItemKey = null
          this.set({ activeItemKey })
          smoothScroll(getScrollContainer(), 0)
          return
        }
        if (index === -1) {
          let { first, firstComplete } = firstVisibleElementIndex(elements)
          index = (movement > 0) ? firstComplete : first
        } else {
          index += movement
        }
        if (index >= 0 && index < elements.length) {
          activeItemKey = elementToKey(elements[index])
          this.setActiveItem(activeItemKey, timeStamp)
          scrollIntoViewIfNeeded(keyToElement(activeItemKey))
        }
      },
      checkActiveItem (timeStamp) {
        let activeElement = document.activeElement
        if (!activeElement) {
          return null
        }
        let activeItem = activeElement.getAttribute('id')
        if (!activeItem) {
          return null
        }
        let { activeItemChangeTime } = this.get()
        if ((timeStamp - activeItemChangeTime) > VISIBILITY_CHECK_DELAY_MS &&
            !isVisible(keyToElement(activeItem))) {
          this.setActiveItem(null, 0)
          return null
        }
        return activeItem
      },
      setActiveItem (key, timeStamp) {
        this.set({ activeItemChangeTime: timeStamp })
        try {
          keyToElement(key).focus({
            preventScroll: true
          })
        } catch (err) {
          console.error('Ignored focus error', err)
        }
      }
    }
  }
</script>


code:
<script>
  import {
    addToShortcutScope,
    removeFromShortcutScope } from '../../_utils/shortcuts'
  export default {
    data: () => ({ scope: 'global', key: null }),
    oncreate () {
      let { scope, key } = this.get()
      addToShortcutScope(scope, key, this)
    },
    ondestroy () {
      let { scope, key } = this.get()
      removeFromShortcutScope(scope, key, this)
    },
    methods: {
      onKeyDown (event) {
        event.stopPropagation()
        event.preventDefault()
        this.fire('pressed', {
          key: event.key,
          timeStamp: event.timeStamp })
      }
    }
  }
</script>


code:
      {text}

      <button class="button" on:click="onClick(event)">
        {buttonText}
      </button>
      <button class="button" aria-label="Close" on:click="close(event)">
        <SvgIcon className="close-snackbar-button" href="#fa-times" />
      </button>


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

let snackbar

const lazySnackbar = {
  async announce (text, buttonText, buttonAction) {
    if (!snackbar) {
      let Snackbar = await importSnackbar()
      if (!snackbar) {
        snackbar = new Snackbar({
          target: document.querySelector('#theSnackbar')
        })
        if (process.env.NODE_ENV !== 'production') {
          window.snackbar = snackbar // for debugging
        }
      }
    }
    snackbar.announce(text, buttonText, buttonAction)
  }
}

export { lazySnackbar as snackbar }


code:
{#if type === 'video'}
  <button id={elementId}
          type="button"
          class="play-video-button {$largeInlineMedia ? '' : 'fixed-size'}"
          aria-label="Play video: {description}"
          style="width: {inlineWidth}px; height: {inlineHeight}px;">
    <PlayVideoIcon />
    <LazyImage
      alt={description}
      title={description}
      src={previewUrl}
      fallback={oneTransparentPixel}
      width={inlineWidth}
      height={inlineHeight}
      background="var(--loading-bg)"
      {focus}
    />
  </button>
{:else}
  <button id={elementId}
          type="button"
          class="show-image-button {$largeInlineMedia ? '' : 'fixed-size'}"
          aria-label="Show image: {description}"
          title={description}
          on:mouseover="set({mouseover: event})"
          style="width: {inlineWidth}px; height: {inlineHeight}px;">
  {#if type === 'gifv' && $autoplayGifs}
    <AutoplayVideo
      ariaLabel="Animated GIF: {description}"
      poster={previewUrl}
      src={url}
      width={inlineWidth}
      height={inlineHeight}
      {focus}
    />
  {:elseif type === 'gifv' && !$autoplayGifs}
    <NonAutoplayGifv
      class={noNativeWidthHeight ? 'no-native-width-height' : ''}
      label="Animated GIF: {description}"
      poster={previewUrl}
      src={url}
      staticSrc={previewUrl}
      width={inlineWidth}
      height={inlineHeight}
      playing={mouseover}
      {focus}
    />
  {:else}
    <LazyImage
      alt={description}
      title={description}
      src={previewUrl}
      fallback={oneTransparentPixel}
      width={inlineWidth}
      height={inlineHeight}
      background="var(--loading-bg)"
      {focus}
    />
  {/if}
</button>
{/if}
<style>

  :global(.status-media video, .status-media img) {
    object-fit: cover;
  }
  .play-video-button, .show-image-button {
    margin: auto;
    padding: 0;
    border-radius: 0;
    border: none;
    background: none;
    position: relative;
  }

  /* the actual focus outline is not very visible, so use an ::after pseudo-element */
  .play-video-button:focus, .show-image-button:focus {
    outline: none;
  }

  .play-video-button:focus::after, .show-image-button:focus::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;
    border: 2px solid var(--focus-outline);
  }

  .show-image-button {
    cursor: zoom-in;
  }
</style>
<script>
  import { DEFAULT_MEDIA_WIDTH, DEFAULT_MEDIA_HEIGHT, ONE_TRANSPARENT_PIXEL } from '../../_static/media'
  import { importShowMediaDialog } from '../dialog/asyncDialogs'
  import { mouseover } from '../../_utils/events'
  import NonAutoplayGifv from '../NonAutoplayGifv.html'
  import PlayVideoIcon from '../PlayVideoIcon.html'
  import { store } from '../../_store/store'
  import LazyImage from '../LazyImage.html'
  import AutoplayVideo from '../AutoplayVideo.html'
  import { registerClickDelegate } from '../../_utils/delegate'

  export default {
    oncreate () {
      let { elementId } = this.get()
      registerClickDelegate(this, elementId, () => this.onClick())
    },
    computed: {
      focus: ({ meta }) => meta && meta.focus,
      // width/height to show inline
      inlineWidth: ({ smallWidth, $largeInlineMedia }) => {
        if (!$largeInlineMedia) {
          return '100%'
        }
        return smallWidth || DEFAULT_MEDIA_WIDTH
      },
      inlineHeight: ({ smallHeight, $largeInlineMedia }) => {
        if (!$largeInlineMedia) {
          return 'auto'
        }
        return smallHeight || DEFAULT_MEDIA_HEIGHT
      },
      // width/height to show in a modal
      modalWidth: ({ originalWidth, smallWidth }) => originalWidth || smallWidth || 0,
      modalHeight: ({ originalHeight, smallHeight }) => originalHeight || smallHeight || 0,
      meta: ({ media }) => media.meta,
      small: ({ meta }) => meta && meta.small,
      original: ({ meta }) => meta && meta.original,
      smallWidth: ({ small }) => small && small.width,
      smallHeight: ({ small }) => small && small.height,
      originalWidth: ({ original }) => original && original.width,
      originalHeight: ({ original }) => original && original.height,
      noNativeWidthHeight: ({ smallWidth, smallHeight }) => typeof smallWidth !== 'number' || typeof smallHeight !== 'number',
      elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
      description: ({ media }) => media.description || '',
      previewUrl: ({ media }) => media.preview_url,
      url: ({ media }) => media.url,
      type: ({ media }) => media.type
    },
    methods: {
      async onClick () {
        let { mediaAttachments, index } = this.get()
        let showMediaDialog = await importShowMediaDialog()
        showMediaDialog(mediaAttachments, index)
      }
    },
    data: () => ({
      oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
      mouseover: void 0
    }),
    store: () => store,
    events: {
      mouseover
    },
    components: {
      NonAutoplayGifv,
      PlayVideoIcon,
      LazyImage,
      AutoplayVideo
    }
  }
</script>


code:
<div class={computedClass}
     style="grid-template-columns: repeat({nCols}, 1fr);" >
  {#each mediaAttachments as media, index}
    <Media {media} {uuid} {mediaAttachments} {index} />
  {/each}
</div>
<style>
  .status-media {
    grid-area: media;
    display: grid;
    align-items: center;
    justify-content: center;
    justify-items: center;
    grid-column-gap: 5px;
    grid-row-gap: 5px;

    overflow: hidden;
    margin: 10px 10px 10px 5px;
  }

  .status-media.grouped-images {
    grid-area: media-grp;
    border-radius: 6px;
  }

  .status-media.grouped-images > :global(*) {
      padding-bottom: 56.25%;
      width: 100%;
  }

  .status-media.grouped-images.two-cols > :global(*) {
      padding-bottom: calc(112.5% + 5px);
  }

  .status-media.grouped-images.odd-cols > :global(:first-child) {
      grid-row-end: span 2;
      padding-bottom: calc(112.5% + 5px);
      background-color: blue;
  }

  .status-media.status-media-is-sensitive {
    margin: 0;
  }
</style>
<script>
  import Media from './Media.html'
  import { classname } from '../../_utils/classname'

  export default {
    computed: {
      computedClass: ({ sensitive, $largeInlineMedia, oddCols, twoCols }) => classname(
        'status-media',
        sensitive && 'status-media-is-sensitive',
        oddCols && 'odd-cols',
        twoCols && 'two-cols',
        !$largeInlineMedia && 'grouped-images'
      ),
      nCols:
        ({ mediaAttachments, $largeInlineMedia }) => {
          return (!$largeInlineMedia && mediaAttachments.length > 1) ? 2 : 1
        },
      oddCols:
        ({ mediaAttachments }) => {
          return (mediaAttachments.length > 1 && (mediaAttachments.length % 2))
        },

      twoCols:
        ({ mediaAttachments }) => {
          return (mediaAttachments.length === 2)
        }
    },
    components: {
      Media
    }
  }
</script>


code:
{#if status}
  <Status {index} {length} {timelineType} {timelineValue} {focusSelector}
          {status} {notification} {enableShortcuts} on:recalculateHeight
  />
{:else}
  <article id={elementId}
           class={className}
           tabindex="0"
           aria-posinset={index + 1}
           aria-setsize={length}
           aria-label={ariaLabel}
           on:applyFocusStylesToParent="noop()"
  >
    <StatusHeader {notification} {notificationId} {status} {statusId} {timelineType}
                  {account} {accountId} {uuid} isStatusInNotification="true" />
    {#if enableShortcuts}
      <Shortcut scope={shortcutScope} key="p" on:pressed="openAuthorProfile()" />
      <Shortcut scope={shortcutScope} key="m" on:pressed="mentionAuthor()" />
    {/if}
  </article>
{/if}
<style>
  .notification-article {
    width: 560px;
    max-width: calc(100vw - 40px);
    padding: 10px 20px;
    border-bottom: 1px solid var(--main-border);
  }
  .notification-article:focus {
    outline: none; /* focus is on the parent instead */
  }
  @media (max-width: 767px) {
    .notification-article {
      padding: 10px 10px;
      max-width: calc(100vw - 20px);
      width: 580px;
    }
  }
</style>
<script>
  import Status from './Status.html'
  import StatusHeader from './StatusHeader.html'
  import Shortcut from '../shortcut/Shortcut.html'
  import { store } from '../../_store/store'
  import { getAccountAccessibleName } from '../../_a11y/getAccountAccessibleName'
  import { goto } from '../../../../__sapper__/client'
  import { composeNewStatusMentioning } from '../../_actions/mention'
  import { classname } from '../../_utils/classname'
  import { applyFocusStylesToParent } from '../../_utils/events'
  import noop from 'lodash-es/noop'
  import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'

  export default {
    components: {
      Status,
      StatusHeader,
      Shortcut
    },
    data: () => ({
      enableShortcuts: null
    }),
    store: () => store,
    computed: {
      account: ({ notification }) => notification.account,
      accountId: ({ account }) => account.id,
      notificationId: ({ notification }) => notification.id,
      status: ({ notification }) => notification.status,
      statusId: ({ status }) => status && status.id,
      uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => (
        createStatusOrNotificationUuid($currentInstance, timelineType, timelineValue, notificationId, statusId)
      ),
      elementId: ({ uuid }) => uuid,
      shortcutScope: ({ elementId }) => elementId,
      ariaLabel: ({ status, account, $omitEmojiInDisplayNames }) => (
        !status && `${getAccountAccessibleName(account, $omitEmojiInDisplayNames)} followed you, @${account.acct}`
      ),
      className: ({ $underlineLinks }) => (classname(
        'notification-article',
        'shortcut-list-item',
        $underlineLinks && 'underline-links'
      ))
    },
    methods: {
      noop,
      openAuthorProfile () {
        let { accountId } = this.get()
        goto(`/accounts/${accountId}`)
      },
      async mentionAuthor () {
        let { account } = this.get()
        await composeNewStatusMentioning(account)
      }
    },
    events: {
      applyFocusStylesToParent
    }
  }
</script>


code:
<article id={elementId}
         class={className}
         tabindex="0"
         aria-posinset={index + 1}
         aria-setsize={length}
         aria-label={ariaLabel}
         on:recalculateHeight
         on:applyFocusStylesToParent="noop()"
>
  {#if showHeader}
    <StatusHeader {...params} />
  {/if}
  <StatusAuthorName {...params} />
  <StatusAuthorHandle {...params} />
  {#if !isStatusInOwnThread}
    <StatusRelativeDate {...params} {...timestampParams} />
  {/if}
  <StatusSidebar {...params} />
  {#if spoilerText}
    <StatusSpoiler {...params} {spoilerShown} on:recalculateHeight />
  {/if}
  {#if !showContent}
    <StatusMentions {...params} />
  {/if}
  {#if content && (showContent || contentPreloaded)}
    <StatusContent {...params} shown={showContent}/>
  {/if}
  {#if showCard }
    <StatusCard {...params} />
  {/if}
  {#if showMedia }
    <StatusMediaAttachments {...params} on:recalculateHeight />
  {/if}
  {#if showPoll}
    <StatusPoll {...params} />
  {/if}
  {#if isStatusInOwnThread}
    <StatusDetails {...params} {...timestampParams} />
  {/if}
  <StatusToolbar {...params} {replyShown} on:recalculateHeight />
  {#if replyShown}
    <StatusComposeBox {...params} on:recalculateHeight />
  {/if}
</article>
{#if enableShortcuts}
  <Shortcut scope={shortcutScope} key="o" on:pressed="open()" />
  <Shortcut scope={shortcutScope} key="p" on:pressed="openAuthorProfile()" />
  <Shortcut scope={shortcutScope} key="m" on:pressed="mentionAuthor()" />
{/if}

<style>
  .status-article {
    padding: 10px 20px;
    display: grid;
    grid-template-areas:
        "header  header       header        header"
        "sidebar author-name  author-handle relative-date"
        "sidebar spoiler      spoiler       spoiler"
        "sidebar spoiler-btn  spoiler-btn   spoiler-btn"
        "sidebar mentions     mentions      mentions"
        "sidebar content      content       content"
        "sidebar card         card          card"
        "sidebar media-grp    media-grp     media-grp"
        "sidebar poll         poll          poll"
        "media   media        media         media"
        "....... toolbar      toolbar       toolbar"
        "compose compose      compose       compose";
    grid-template-columns: min-content minmax(0, max-content) 1fr min-content;
    grid-template-rows: repeat(8, max-content);
  }

  .status-article.tap-on-status {
    cursor: pointer;
  }

  .status-article.status-in-timeline {
    border-bottom: 1px solid var(--main-border);
  }

  .status-article.status-direct {
    background-color: var(--status-direct-background);
  }

  .status-article:focus {
    outline: none; /* focus is on the parent instead */
  }

  .status-article.status-in-own-thread {
    grid-template-areas:
      "sidebar     author-name"
      "sidebar     author-handle"
      "spoiler     spoiler"
      "spoiler-btn spoiler-btn"
      "mentions    mentions"
      "content     content"
      "card        card"
      "media-grp   media-grp"
      "media       media"
      "poll        poll"
      "details     details"
      "toolbar     toolbar"
      "compose     compose";
    grid-template-columns: min-content 1fr;
    grid-template-rows: repeat(7, max-content);
  }

  @media (max-width: 767px) {
    .status-article {
      padding: 10px 10px;
    }
  }
</style>
<script>
  import StatusSidebar from './StatusSidebar.html'
  import StatusHeader from './StatusHeader.html'
  import StatusAuthorName from './StatusAuthorName.html'
  import StatusAuthorHandle from './StatusAuthorHandle.html'
  import StatusRelativeDate from './StatusRelativeDate.html'
  import StatusDetails from './StatusDetails.html'
  import StatusCard from './StatusCard.html'
  import StatusToolbar from './StatusToolbar.html'
  import StatusMediaAttachments from './StatusMediaAttachments.html'
  import StatusContent from './StatusContent.html'
  import StatusSpoiler from './StatusSpoiler.html'
  import StatusComposeBox from './StatusComposeBox.html'
  import StatusMentions from './StatusMentions.html'
  import StatusPoll from './StatusPoll.html'
  import Shortcut from '../shortcut/Shortcut.html'
  import { store } from '../../_store/store'
  import { goto } from '../../../../__sapper__/client'
  import { registerClickDelegate } from '../../_utils/delegate'
  import { classname } from '../../_utils/classname'
  import { checkDomAncestors } from '../../_utils/checkDomAncestors'
  import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
  import { getAccountAccessibleName } from '../../_a11y/getAccountAccessibleName'
  import { getAccessibleLabelForStatus } from '../../_a11y/getAccessibleLabelForStatus'
  import { formatTimeagoDate } from '../../_intl/formatTimeagoDate'
  import { measureText } from '../../_utils/measureText'
  import { LONG_POST_LENGTH, LONG_POST_TEXT } from '../../_static/statuses'
  import { absoluteDateFormatter } from '../../_utils/formatters'
  import { composeNewStatusMentioning } from '../../_actions/mention'
  import { applyFocusStylesToParent } from '../../_utils/events'
  import noop from 'lodash-es/noop'
  import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
  import { statusHtmlToPlainText } from '../../_utils/statusHtmlToPlainText'

  const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
  const isUserInputElement = node => INPUT_TAGS.has(node.localName)
  const isToolbar = node => node.classList.contains('status-toolbar')
  const isStatusArticle = node => node.classList.contains('status-article')

  export default {
    oncreate () {
      let { elementId, isStatusInOwnThread, showContent } = this.get()
      let { disableTapOnStatus } = this.store.get()
      if (!isStatusInOwnThread && !disableTapOnStatus) {
        // the whole <article> is clickable in this case
        registerClickDelegate(this, elementId, (e) => this.onClickOrKeydown(e))
      }
      if (!showContent) {
        scheduleIdleTask(() => {
          // Perf optimization: lazily load the StatusContent when the user is idle so that
          // it's fast when they click the "show more" button
          this.set({ contentPreloaded: true })
        })
      }
    },
    components: {
      StatusSidebar,
      StatusHeader,
      StatusAuthorName,
      StatusAuthorHandle,
      StatusRelativeDate,
      StatusDetails,
      StatusToolbar,
      StatusMediaAttachments,
      StatusContent,
      StatusCard,
      StatusPoll,
      StatusSpoiler,
      StatusComposeBox,
      StatusMentions,
      Shortcut
    },
    data: () => ({
      notification: void 0,
      replyVisibility: void 0,
      contentPreloaded: false,
      enableShortcuts: null
    }),
    store: () => store,
    methods: {
      noop,
      onClickOrKeydown (e) {
        let { type, keyCode, target } = e

        let isClick = type === 'click'
        let isEnter = type === 'keydown' && keyCode === 13
        if (!isClick && !isEnter) {
          return
        }
        if (checkDomAncestors(target, isUserInputElement, isStatusArticle)) {
          // this element or any ancestors up to the status-article element is
          // a user input element
          return
        }
        if (checkDomAncestors(target, isToolbar, isStatusArticle)) {
          // this element or any of its ancestors is the toolbar
          return
        }

        e.preventDefault()
        e.stopPropagation()
        this.open()
      },
      open () {
        let { originalStatusId } = this.get()
        goto(`/statuses/${originalStatusId}`)
      },
      openAuthorProfile () {
        let { accountForShortcut } = this.get()
        goto(`/accounts/${accountForShortcut.id}`)
      },
      async mentionAuthor () {
        let { accountForShortcut } = this.get()
        await composeNewStatusMentioning(accountForShortcut)
      }
    },
    computed: {
      originalStatus: ({ status }) => status.reblog ? status.reblog : status,
      originalStatusId: ({ originalStatus }) => originalStatus.id,
      statusId: ({ status }) => status.id,
      notificationId: ({ notification }) => notification && notification.id,
      account: ({ notification, status }) => (
        (notification && notification.account) || status.account
      ),
      accountId: ({ account }) => account.id,
      originalAccount: ({ originalStatus }) => originalStatus.account,
      originalAccountId: ({ originalAccount }) => originalAccount.id,
      accountForShortcut: ({ originalAccount, notification }) => notification ? notification.account : originalAccount,
      visibility: ({ originalStatus }) => originalStatus.visibility,
      mentions: ({ originalStatus }) => originalStatus.mentions || [],
      plainTextContent: ({ content, mentions }) => statusHtmlToPlainText(content, mentions),
      plainTextContentLength: ({ plainTextContent }) => measureText(plainTextContent),
      plainTextContentOverLength: ({ plainTextContentLength }) => plainTextContentLength > LONG_POST_LENGTH,
      spoilerText: ({ originalStatus, plainTextContentOverLength }) => (
        originalStatus.spoiler_text || (plainTextContentOverLength && LONG_POST_TEXT)
      ),
      inReplyToId: ({ originalStatus }) => originalStatus.in_reply_to_id,
      uuid: ({ $currentInstance, timelineType, timelineValue, notificationId, statusId }) => (
        createStatusOrNotificationUuid($currentInstance, timelineType, timelineValue, notificationId, statusId)
      ),
      elementId: ({ uuid }) => uuid,
      shortcutScope: ({ elementId }) => elementId,
      isStatusInOwnThread: ({ timelineType, timelineValue, originalStatusId }) => (
        (timelineType === 'status' || timelineType === 'reply') && timelineValue === originalStatusId
      ),
      isStatusInNotification: ({ originalStatusId, notification }) => (
        notification && notification.status &&
          notification.type !== 'mention' && notification.status.id === originalStatusId
      ),
      spoilerShown: ({ $spoilersShown, uuid }) => !!$spoilersShown[uuid],
      replyShown: ({ $repliesShown, uuid }) => !!$repliesShown[uuid],
      showCard: ({ originalStatus, isStatusInNotification, showMedia, $hideCards }) => (
        !$hideCards &&
        !isStatusInNotification &&
        !showMedia &&
        originalStatus.card &&
        originalStatus.card.title
      ),
      showPoll: ({ originalStatus }) => (
        originalStatus.poll
      ),
      showMedia: ({ originalStatus, isStatusInNotification }) => (
        !isStatusInNotification &&
        originalStatus.media_attachments &&
        originalStatus.media_attachments.length
      ),
      originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
      originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
      originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
      originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
        return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
      },
      createdAtDate: ({ originalStatus }) => originalStatus.created_at,
      createdAtDateTS: ({ createdAtDate }) => new Date(createdAtDate).getTime(),
      absoluteFormattedDate: ({ createdAtDateTS }) => absoluteDateFormatter.format(createdAtDateTS),
      timeagoFormattedDate: ({ createdAtDateTS, $now }) => formatTimeagoDate(createdAtDateTS, $now),
      reblog: ({ status }) => status.reblog,
      ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
        showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
        getAccessibleLabelForStatus(originalAccount, account, plainTextContent,
          timeagoFormattedDate, spoilerText, showContent,
          reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
      ),
      showHeader: ({ notification, status, timelineType }) => (
        (notification && ['reblog', 'favourite', 'poll'].includes(notification.type)) ||
        status.reblog ||
        timelineType === 'pinned'
      ),
      className: ({ visibility, timelineType, isStatusInOwnThread, $underlineLinks, $disableTapOnStatus }) => (classname(
        'status-article',
        'shortcut-list-item',
        timelineType !== 'direct' && visibility === 'direct' && 'status-direct',
        timelineType !== 'search' && 'status-in-timeline',
        isStatusInOwnThread && 'status-in-own-thread',
        $underlineLinks && 'underline-links',
        !$disableTapOnStatus && !isStatusInOwnThread && 'tap-on-status'
      )),
      content: ({ originalStatus }) => originalStatus.content || '',
      showContent: ({ spoilerText, spoilerShown }) => !spoilerText || spoilerShown,
      // These timestamp params may change every 10 seconds due to now() polling, so keep them
      // separate from the generic `params` list to avoid costly recomputes.
      timestampParams: ({ createdAtDate, createdAtDateTS, timeagoFormattedDate, absoluteFormattedDate }) => ({
        createdAtDate,
        createdAtDateTS,
        timeagoFormattedDate,
        absoluteFormattedDate
      }),
      // This params list deliberately does *not* include `spoilersShown` or `replyShown`, because these
      // change frequently and would therefore cause costly recomputes if included here.
      // The main goal here is to avoid typing by passing as many params as possible to child components.
      params: ({ notification, notificationId, status, statusId, timelineType,
        account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
        originalAccount, originalAccountId, visibility,
        replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
        enableShortcuts, shortcutScope, originalStatusEmojis }) => ({
        notification,
        notificationId,
        status,
        statusId,
        timelineType,
        account,
        accountId,
        uuid,
        isStatusInNotification,
        isStatusInOwnThread,
        originalAccount,
        originalAccountId,
        visibility,
        replyVisibility,
        spoilerText,
        originalStatus,
        originalStatusId,
        inReplyToId,
        enableShortcuts,
        shortcutScope,
        originalStatusEmojis
      })
    },
    events: {
      applyFocusStylesToParent
    }
  }
</script>


code:
<span class="status-author-handle {isStatusInNotification ? 'status-in-notification' : '' }">
  {'@' + originalAccount.acct}
</span>
<style>
  .status-author-handle {
    grid-area: author-handle;
    align-self: center;
    margin-left: 5px;
    color: var(--deemphasized-text-color);
    font-size: 1.1em;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .status-author-handle.status-in-notification {
    color: var(--very-deemphasized-text-color);
  }
</style>


code:
<a id={elementId}
   class="status-author-name {isStatusInNotification ? 'status-in-notification' : '' } {isStatusInOwnThread ? 'status-in-own-thread' : ''}"
   rel="prefetch"
   href="/accounts/{originalAccountId}"
   title="{'@' + originalAccount.acct}"
>
  <AccountDisplayName account={originalAccount} />
</a>
<style>
  .status-author-name {
    grid-area: author-name;
    align-self: center;
    margin-left: 5px;
    font-size: 1.1em;
    min-width: 0;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .status-author-name.status-in-own-thread {
    font-size: 1.3em;
  }

  .status-author-name, .status-author-name:hover, .status-author-name:visited {
    color: var(--body-text-color);
  }

  .status-author-name.status-in-notification,
  .status-author-name.status-in-notification:hover,
  .status-author-name.status-in-notification:visited {
    color: var(--very-deemphasized-text-color);
  }

</style>
<script>
  import AccountDisplayName from '../profile/AccountDisplayName.html'

  export default {
    computed: {
      elementId: ({ uuid }) => `status-author-name-${uuid}`
    },
    components: {
      AccountDisplayName
    }
  }
</script>


code:
     {unescapedTitle}

  {#if description}

  {#if imageUrl}

      <LazyImage src={imageUrl} ariaHidden={true} />

  {/if}

      {unescapedDescription}

  {/if}
  {#if enableShortcuts}
  <Shortcut scope={shortcutScope} key="l" on:pressed="open(url)" />
  {/if}


code:
<div class="status-article-compose-box">
  <ComposeBox realm={originalStatusId}
              autoFocus="true"
              hideBottomBorder="true"
              isReply="true"
              replyVisibility={visibility}
              replySpoiler={spoilerText}
              inReplyToUuid={uuid}
  />
</div>
<style>
  .status-article-compose-box {
    grid-area: compose;
    max-width: 100%;
  }
</style>
<script>
  import ComposeBox from '../compose/ComposeBox.html'
  import { store } from '../../_store/store'
  import debounce from 'lodash-es/debounce'
  import throttle from 'lodash-es/throttle'
  import { on } from '../../_utils/eventBus'
  import { observe } from 'svelte-extras'

  const DEBOUNCE_DELAY = 400
  const THROTTLE_DELAY = 150

  export default {
    oncreate () {
      on('postedStatus', this, this.onPostedStatus)
      this.setupRecalculateHeightListener()
    },
    store: () => store,
    computed: {
      composeData: ({ $currentComposeData, originalStatusId }) => $currentComposeData[originalStatusId] || {}
    },
    methods: {
      observe,
      onPostedStatus (realm) {
        let { originalStatusId } = this.get()
        if (realm !== originalStatusId) {
          return
        }
        requestAnimationFrame(() => {
          let { uuid } = this.get()
          let { repliesShown } = this.store.get()
          repliesShown[uuid] = false
          this.store.set({ repliesShown })
          this.fire('recalculateHeight')
        })
      },
      setupRecalculateHeightListener () {
        const recalc = () => requestAnimationFrame(() => this.fire('recalculateHeight'))
        // debounce AND throttle due to 333ms content warning animation
        const debounced = debounce(recalc, DEBOUNCE_DELAY)
        const throttled = throttle(() => {
          debounced()
          recalc()
        }, THROTTLE_DELAY, {
          leading: true,
          trailing: true
        })
        this.observe('composeData', throttled)
      }
    },
    components: {
      ComposeBox
    }
  }
</script>


code:
<div class={computedClass} ref:node>
    {@html massagedContent}
</div>
<style>
  .status-content {
    margin: 10px 10px 10px 5px;
    grid-area: content;
    word-wrap: break-word;
    overflow: hidden;
    white-space: pre-wrap;
    font-size: 0.9em;
    display: none;
  }

  .status-content.status-in-own-thread {
    font-size: 1.3em;
    margin: 20px 10px 20px 5px;
  }

  .status-content.shown {
    display: block;
  }

  :global(.status-content p) {
    margin: 0 0 20px;
  }

  :global(.status-content p:first-child) {
    margin: 0 0 20px;
  }

  :global(.status-content p:last-child) {
    margin: 0;
  }

  .status-content.status-in-notification {
    color: var(--very-deemphasized-text-color);
  }
  :global(.status-content.status-in-notification a, .status-content.status-in-notification a:hover) {
    color: var(--very-deemphasized-link-color);
  }

  :global(.status-content .invisible) {
    /* copied from Mastodon */
    font-size: 0;
    line-height: 0;
    display: inline-block;
    width: 0;
    height: 0;
    position: absolute;
  }

  :global(.underline-links .status-content a) {
    text-decoration: underline;
  }

</style>
<script>
  import { mark, stop } from '../../_utils/marks'
  import { store } from '../../_store/store'
  import { classname } from '../../_utils/classname'
  import { massageUserText } from '../../_utils/massageUserText'

  export default {
    oncreate () {
      this.hydrateContent()
    },
    store: () => store,
    computed: {
      computedClass: ({ isStatusInOwnThread, isStatusInNotification, shown }) => {
        return classname(
          'status-content',
          isStatusInOwnThread && 'status-in-own-thread',
          isStatusInNotification && 'status-in-notification',
          shown && 'shown'
        )
      },
      content: ({ originalStatus }) => (originalStatus.content || ''),
      massagedContent: ({ content, originalStatusEmojis, $autoplayGifs }) => (
        massageUserText(content, originalStatusEmojis, $autoplayGifs)
      )
    },
    methods: {
      hydrateContent () {
        mark('hydrateContent')
        let node = this.refs.node
        let { originalStatus, uuid } = this.get()
        let { mentions, tags } = originalStatus
        let count = 0
        let anchors = Array.from(node.getElementsByTagName('A'))

        for (let anchor of anchors) {
          // hydrate hashtag
          if (tags && anchor.classList.contains('hashtag')) {
            for (let tag of tags) {
              if (anchor.getAttribute('href').endsWith(`/tags/${tag.name}`)) {
                anchor.setAttribute('href', `/tags/${tag.name}`)
                anchor.setAttribute('id', `status-content-link-${uuid}-${++count}`)
                anchor.removeAttribute('target')
                anchor.removeAttribute('rel')
              }
            }
          // hydrate mention
          } else if (mentions && anchor.classList.contains('mention')) {
            for (let mention of mentions) {
              if (anchor.getAttribute('href') === mention.url) {
                anchor.setAttribute('href', `/accounts/${mention.id}`)
                anchor.setAttribute('title', `@${mention.acct}`)
                anchor.setAttribute('id', `status-content-link-${uuid}-${++count}`)
                anchor.removeAttribute('target')
                anchor.removeAttribute('rel')
              }
            }
          }
          // hydrate external links
          let href = anchor.getAttribute('href')
          if (new URL(href, location.href).origin !== location.origin) {
            anchor.setAttribute('title', href)
            anchor.setAttribute('target', '_blank')
            anchor.setAttribute('rel', 'nofollow noopener')
          }
        }
        stop('hydrateContent')
      }
    }
  }
</script>


code:
  <ExternalLink className="status-absolute-date"
                href={originalStatus.url}
                showIcon={true}
                ariaLabel="{displayAbsoluteFormattedDate} (opens in new window)"
  >
    <time datetime={createdAtDate} title={absoluteFormattedDate}>
      {displayAbsoluteFormattedDate}
    </time>
  </ExternalLink>
  {#if applicationName}
    {#if applicationWebsite}
      <ExternalLink className="status-application"
                    href={applicationWebsite}
                    showIcon={false}
                    ariaLabel="{applicationName} (opens in new window)">

          {applicationName}

      </ExternalLink>
    {:else}

        {applicationName}

    {/if}
  {/if}

    <SvgIcon className="status-favs-reblogs-svg" href="#fa-retweet" />
    {numReblogs}

    <SvgIcon className="status-favs-reblogs-svg" href="#fa-star" />
    {numFavs}


code:
    <Avatar {account} size="extra-small"/>

  <SvgIcon className="status-header-svg" href={icon} />

    {#if timelineType === 'pinned'}

        Pinned toot

    {:elseif notificationType !== 'poll'}

        <AccountDisplayName {account} />

    {/if}

    {actionText}


code:
{#if sensitive }

  {#if sensitiveShown}
  <button id={elementId}
          type="button"
          class="status-sensitive-media-button"
          aria-label="Hide sensitive media" >

      <SvgIcon className="status-sensitive-media-svg" href="#fa-eye-slash" />

  </button>
  <MediaAttachments {mediaAttachments} {sensitive} {uuid} />
  {:else}
    <button id={elementId}
            type="button"
            class="status-sensitive-media-button"
            aria-label="Show sensitive media" >

        Sensitive content. Click to show.

        <SvgIcon className="status-sensitive-media-svg" href="#fa-eye" />

    </button>
  {/if}

{#if enableShortcuts}
  <Shortcut scope={shortcutScope} key="y" on:pressed="toggleSensitiveMedia()"/>
{/if}
{:else}
  <MediaAttachments {mediaAttachments} {sensitive} {uuid} />
{/if}


code:
{#if mentions.length}

      {#each mentions as mention, i}
        {#if i > 0}

        {/if}

          @{mention.username}

      {/each}

{/if}


code:
  {#if voted || expired }

      {#each options as option}

            {option.share}% {option.title}

          <svg aria-hidden="true">
            <line x1="0" y1="0" x2="{option.share}%" y2="0" />
          </svg>

      {/each}

  {:else}
    <form class="poll-form" aria-label="Vote on poll" on:submit="onSubmit(event)" ref:form>

        {#each options as option, i}

            <input type="{multiple ? 'checkbox' : 'radio'}"
                   id="poll-choice-{uuid}-{i}"
                   name="poll-choice-{uuid}"
                   value="{i}"
                   on:change="onChange()"
            >

              {option.title}

        {/each}

      <button disabled={formDisabled} type="submit">Vote</button>
    </form>
  {/if}

      <SvgIcon className="poll-icon" href="#fa-bar-chart" />
      {votesCount} {votesCount === 1 ? 'vote' : 'votes'}

      <SvgIcon className="poll-icon" href="#fa-clock" />

        {expiryText}
        <time datetime={expiresAt} title={expiresAtAbsoluteFormatted}>
          {expiresAtTimeagoFormatted}
        </time>

    <button class="poll-stat {expired ? 'poll-expired' : ''}" id={refreshElementId}>
      <SvgIcon className="poll-icon" href="#fa-refresh" />

        Refresh

    </button>


code:
<a id={elementId}
   class="status-relative-date {isStatusInNotification ? 'status-in-notification' : '' }"
   href="/statuses/{originalStatusId}"
   rel="prefetch"
>
  <time datetime={createdAtDate} title={absoluteFormattedDate}
        aria-label="{timeagoFormattedDate} – click to show thread">
    {timeagoFormattedDate}
  </time>
</a>
<style>
  .status-relative-date {
    grid-area: relative-date;
    align-self: center;
    margin-left: 5px;
    margin-right: 10px;
    font-size: 1.1em;
    text-align: right;
    white-space: nowrap;
  }
  .status-relative-date, .status-relative-date:hover, .status-relative-date:visited {
    color: var(--deemphasized-text-color);
  }

  .status-relative-date.status-in-notification,
  .status-relative-date.status-in-notification:hover,
  .status-relative-date.status-in-notification:visited {
    color: var(--very-deemphasized-text-color);
  }

</style>
<script>
  export default {
    computed: {
      elementId: ({ uuid }) => `status-relative-date-${uuid}`
    }
  }
</script>


code:
<a id={elementId}
   class="status-sidebar size-{size}"
   rel="prefetch"
   href="/accounts/{originalAccountId}"
   aria-hidden="true"
>
  <Avatar account={originalAccount}
          isLink="true"
          {size} />
</a>
<style>
  .status-sidebar {
    grid-area: sidebar;
    margin-right: 15px;
  }

  .status-sidebar.size-small {
    width: 48px;
    height: 48px;
  }

  .status-sidebar.size-medium {
    width: 64px;
    height: 64px;
  }

  @media (max-width: 767px) {
    .status-sidebar {
      margin-right: 5px;
    }
  }
</style>
<script>
  import Avatar from '../Avatar.html'
  export default {
    components: {
      Avatar
    },
    computed: {
      elementId: ({ uuid }) => `status-author-avatar-${uuid}`,
      size: ({ isStatusInOwnThread }) => isStatusInOwnThread ? 'medium' : 'small'
    }
  }
</script>


code:
{@html massagedSpoilerText}

  <button id={elementId} type="button" >
    {spoilerShown ? 'Show less' : 'Show more'}
  </button>

{#if enableShortcuts}
<Shortcut scope={shortcutScope} key="x" on:pressed="toggleSpoilers()"/>
{/if}


code:
<div class="status-toolbar {isStatusInOwnThread ? 'status-in-own-thread' : ''}" ref:node>
  <IconButton
    className="status-toolbar-reply-button"
    label={replyLabel}
    pressable="true"
    pressed={replyShown}
    href={replyIcon}
    clickListener={false}
    elementId={replyKey}
    />
  <IconButton
    label={reblogLabel}
    pressable={!reblogDisabled}
    pressed={reblogged}
    disabled={reblogDisabled}
    href={reblogIcon}
    clickListener={false}
    elementId={reblogKey}
    ref:reblogIcon
  />
  <IconButton
    label="Favorite"
    pressable="true"
    pressed={favorited}
    href="#fa-star"
    clickListener={false}
    elementId={favoriteKey}
    ref:favoriteIcon
    />
  <IconButton
    label="Show more options"
    href="#fa-ellipsis-h"
    clickListener={false}
    elementId={optionsKey}
  />
</div>
{#if enableShortcuts}
  <Shortcut scope={shortcutScope} key="f" on:pressed="toggleFavorite()"/>
  <Shortcut scope={shortcutScope} key="r" on:pressed="reply()"/>
  <Shortcut scope={shortcutScope} key="b" on:pressed="reblog()"/>
{/if}
<style>
  .status-toolbar {
    grid-area: toolbar;
    display: flex;
    justify-content: space-between;
  }
  .status-toolbar.status-in-own-thread {
    margin-left: 63px; /* offset to align all toolbar items: 48px avatar + 15px margin-right */
  }

  @media (max-width: 767px) {
    .status-toolbar.status-in-own-thread {
      margin-left: 53px; /* offset to align all toolbar items: 48px avatar + 5px margin-right */
    }
  }

</style>
<script>
  import IconButton from '../IconButton.html'
  import Shortcut from '../shortcut/Shortcut.html'
  import { store } from '../../_store/store'
  import { registerClickDelegates } from '../../_utils/delegate'
  import { setFavorited } from '../../_actions/favorite'
  import { setReblogged } from '../../_actions/reblog'
  import { importShowStatusOptionsDialog } from '../dialog/asyncDialogs'
  import { updateProfileAndRelationship } from '../../_actions/accounts'
  import { FAVORITE_ANIMATION, REBLOG_ANIMATION } from '../../_static/animations'
  import { on } from '../../_utils/eventBus'

  export default {
    oncreate () {
      let {
        favoriteKey,
        reblogKey,
        replyKey,
        optionsKey
      } = this.get()
      registerClickDelegates(this, {
        [favoriteKey]: (e) => {
          e.preventDefault()
          e.stopPropagation()
          this.toggleFavorite()
        },
        [reblogKey]: (e) => {
          e.preventDefault()
          e.stopPropagation()
          this.reblog()
        },
        [replyKey]: (e) => {
          e.preventDefault()
          e.stopPropagation()
          this.reply()
        },
        [optionsKey]: (e) => this.onOptionsClick(e)
      })
      on('postedStatus', this, this.onPostedStatus)
    },
    components: {
      IconButton,
      Shortcut
    },
    store: () => store,
    methods: {
      toggleFavorite () {
        let { originalStatusId, favorited } = this.get()
        let newFavoritedValue = !favorited
        /* no await */ setFavorited(originalStatusId, newFavoritedValue)
        if (newFavoritedValue) {
          this.refs.favoriteIcon.animate(FAVORITE_ANIMATION)
        }
      },
      reblog () {
        let { originalStatusId, reblogged } = this.get()
        let newRebloggedValue = !reblogged
        /* no await */ setReblogged(originalStatusId, newRebloggedValue)
        if (newRebloggedValue) {
          this.refs.reblogIcon.animate(REBLOG_ANIMATION)
        }
      },
      reply () {
        requestAnimationFrame(() => {
          let { uuid } = this.get()
          let { repliesShown } = this.store.get()
          repliesShown[uuid] = !repliesShown[uuid]
          this.store.set({ repliesShown })
          this.fire('recalculateHeight')
        })
      },
      async onOptionsClick (e) {
        e.preventDefault()
        e.stopPropagation()
        let { originalStatus, originalAccountId } = this.get()
        let updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
        let showStatusOptionsDialog = await importShowStatusOptionsDialog()
        await updateRelationshipPromise
        showStatusOptionsDialog(originalStatus)
      },
      onPostedStatus (realm, inReplyToUuid) {
        let {
          originalStatusId,
          uuid
        } = this.get()
        if (realm !== originalStatusId ||
            inReplyToUuid !== uuid) {
          return
        }
        try {
          // return status to the reply button after posting a reply
          this.refs.node.querySelector('.status-toolbar-reply-button').focus({ preventScroll: true })
        } catch (e) { /* ignore */ }
      }
    },
    data: () => ({
      favoriteAnimation: FAVORITE_ANIMATION,
      reblogAnimation: REBLOG_ANIMATION
    }),
    computed: {
      replyLabel: ({ replyShown, inReplyToId }) => (
        replyShown ? 'Close reply' : inReplyToId ? 'Reply to thread' : 'Reply'
      ),
      replyIcon: ({ inReplyToId }) => inReplyToId ? '#fa-reply-all' : '#fa-reply',
      reblogLabel: ({ visibility }) => {
        switch (visibility) {
          case 'private':
            return 'Cannot be boosted because this is followers-only'
          case 'direct':
            return 'Cannot be boosted because this is a direct message'
          default:
            return 'Boost'
        }
      },
      reblogIcon: ({ visibility }) => {
        switch (visibility) {
          case 'private':
            return '#fa-lock'
          case 'direct':
            return '#fa-envelope'
          default:
            return '#fa-retweet'
        }
      },
      reblogDisabled: ({ visibility }) => {
        return visibility === 'private' || visibility === 'direct'
      },
      reblogged: ({ originalStatusId, $currentStatusModifications, originalStatus }) => {
        if ($currentStatusModifications && originalStatusId in $currentStatusModifications.reblogs) {
          return $currentStatusModifications.reblogs[originalStatusId]
        }
        return originalStatus.reblogged
      },
      favorited: ({ originalStatusId, $currentStatusModifications, originalStatus }) => {
        if ($currentStatusModifications && originalStatusId in $currentStatusModifications.favorites) {
          return $currentStatusModifications.favorites[originalStatusId]
        }
        return originalStatus.favourited
      },
      favoriteKey: ({ uuid }) => `fav-${uuid}`,
      reblogKey: ({ uuid }) => `reblog-${uuid}`,
      replyKey: ({ uuid }) => `reply-${uuid}`,
      optionsKey: ({ uuid }) => `options-${uuid}`
    }
  }
</script>


code:
  {#await importTimeline}

  {:then constructor}
    <svelte:component this={constructor} {timeline} />
  {:catch error}

Component failed to load. Try refreshing! {error}

  {/await}


code:
    <LoadingSpinner size={48} />

      Loading more...

    <button type="button"
            class="primary"
            on:click="onClickLoadMore(event)">
      Load more
    </button>


code:
<div class="more-items-header">
  <button class="primary" type="button" on:click="onClick(event)">
    Show {count} more
  </button>
</div>
<style>
  .more-items-header {
    display: flex;
    padding: 5px;
    align-items: center;
    justify-content:center;
  }
</style>
<script>
  export default {
    methods: {
      onClick (event) {
        let { onClick } = this.get()
        if (onClick) {
          onClick(event)
        }
      }
    }
  }
</script>


code:
<MoreHeader count={virtualProps.count}
            onClick={virtualProps.onClick}
/>
<script>
  import MoreHeader from './MoreHeader.html'
  export default {
    components: {
      MoreHeader
    }
  }
</script>


code:
<Notification
  notification={virtualProps.notification}
  timelineType={virtualProps.timelineType}
  timelineValue={virtualProps.timelineValue}
  focusSelector={virtualProps.focusSelector}
  enableShortcuts={true}
  index={virtualIndex}
  length={virtualLength}
  on:recalculateHeight />
<script>
  import Notification from '../status/Notification.html'

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


code:
{#if pinnedStatuses.length }

Pinned statuses

    {#each pinnedStatuses as status, index (status.id)}

        <Status {status}
                timelineType="pinned"
                timelineValue={accountId}
                {index}
                length={pinnedStatuses.length}
                enableShortcuts={true}
        />

    {/each}

{/if}


code:
<Status status={virtualProps.status}
        timelineType={virtualProps.timelineType}
        timelineValue={virtualProps.timelineValue}
        focusSelector={virtualProps.focusSelector}
        enableShortcuts={true}
        index={virtualIndex}
        length={virtualLength}
        on:recalculateHeight />
<script>
  import Status from '../status/Status.html'

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


code:
{label}

  {#await componentsPromise}

  {:then result}
    <svelte:component this={result.listComponent}
                component={result.listItemComponent}
                realm="{$currentInstance + '/' + timeline}"
                {makeProps}
                items={itemIds}
                showFooter={true}
                footerComponent={LoadingFooter}
                showHeader={$showHeader}
                headerComponent={MoreHeaderVirtualWrapper}
                {headerProps}
                {scrollToItem}
                on:scrollToBottom="onScrollToBottom()"
                on:scrollToTop="onScrollToTop()"
                on:scrollTopChanged="onScrollTopChanged(event)"
                on:initialized="initialize()"
                on:noNeedToScroll="onNoNeedToScroll()"
    />
  {:catch error}

Error: component failed to load! Try reloading. {error}

  {/await}

<Shortcut scope="global" key="." on:pressed="showMoreAndScrollToTop()" />
<ScrollListShortcuts />


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

let toast

const lazyToast = {
  async say (text) {
    if (!toast) {
      let Toast = await importToast()
      if (!toast) {
        toast = new Toast({
          target: document.querySelector('#theToast')
        })
        if (process.env.NODE_ENV !== 'production') {
          window.toast = toast // for debugging
        }
      }
    }
    toast.say(text)
  }
}

export { lazyToast as toast }


code:
<VirtualListContainer {realm} on:initialized on:noNeedToScroll >
  <div class="virtual-list"
       style="height: {$height}px;"
       ref:node >
    <VirtualListHeader component={headerComponent} virtualProps={headerProps} shown={$showHeader}/>
    {#if $visibleItems}
      {#each $visibleItems as visibleItem (visibleItem.key)}
        <VirtualListLazyItem {component}
                             offset={visibleItem.offset}
                             {makeProps}
                             key={visibleItem.key}
                             index={visibleItem.index}
        />
      {/each}
    {/if}
    {#if $showFooter}
      <VirtualListFooter component={footerComponent} />
    {/if}
  </div>
</VirtualListContainer>
<style>
  .virtual-list {
    position: relative;
    width: 100%;
  }
</style>
<script>
  import VirtualListContainer from './VirtualListContainer.html'
  import VirtualListLazyItem from './VirtualListLazyItem'
  import VirtualListFooter from './VirtualListFooter.html'
  import VirtualListHeader from './VirtualListHeader.html'
  import { virtualListStore } from './virtualListStore'
  import throttle from 'lodash-es/throttle'
  import { mark, stop } from '../../_utils/marks'
  import isEqual from 'lodash-es/isEqual'
  import { observe } from 'svelte-extras'

  const DISTANCE_FROM_BOTTOM_TO_FIRE = 800
  const SCROLL_EVENT_THROTTLE = 1000

  export default {
    oncreate () {
      this.fireScrollToBottom = throttle(() => {
        this.fire('scrollToBottom')
      }, SCROLL_EVENT_THROTTLE)
      this.fireScrollToTop = throttle(() => {
        this.fire('scrollToTop')
      }, SCROLL_EVENT_THROTTLE)
      this.observe('showFooter', showFooter => {
        mark('set showFooter')
        this.store.setForRealm({ showFooter: showFooter })
        mark('set showFooter')
      })
      this.observe('showHeader', showHeader => {
        mark('set showHeader')
        this.store.setForRealm({ showHeader: showHeader })
        stop('set showHeader')
      })
      this.observe('items', (newItems, oldItems) => {
        if (!newItems || isEqual(newItems, oldItems)) {
          return
        }
        mark('set items')
        this.store.setForRealm({ items: newItems })
        stop('set items')
      })
      this.observe('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight => {
        this.calculateListOffset()
        if (allVisibleItemsHaveHeight) {
          this.fire('initializedVisibleItems')
        }
      })

      let observedOnce = false

      this.observe('distanceFromBottom', (distanceFromBottom) => {
        if (!observedOnce) {
          observedOnce = true // TODO: the first time is always 0... need better way to handle this
          return
        }
        if (distanceFromBottom >= 0 &&
            distanceFromBottom <= DISTANCE_FROM_BOTTOM_TO_FIRE) {
          this.fireScrollToBottom()
        }
      })

      this.observe('scrollTop', (scrollTop) => {
        this.fire('scrollTopChanged', scrollTop)
        if (scrollTop === 0) {
          this.fireScrollToTop()
        }
        this.calculateListOffset()
      })
    },
    data: () => ({
      component: null
    }),
    store: () => virtualListStore,
    components: {
      VirtualListContainer,
      VirtualListLazyItem,
      VirtualListFooter,
      VirtualListHeader
    },
    computed: {
      distanceFromBottom: ({ $scrollHeight, $scrollTop, $offsetHeight }) => {
        return $scrollHeight - $scrollTop - $offsetHeight
      },
      scrollTop: ({ $scrollTop }) => $scrollTop,
      // TODO: bug in svelte store, shouldn't need to do this
      allVisibleItemsHaveHeight: ({ $allVisibleItemsHaveHeight }) => $allVisibleItemsHaveHeight,
      visibleItemKeys: ({ $visibleItems }) => ($visibleItems || []).map(_ => _.key)
    },
    methods: {
      observe,
      calculateListOffset () {
        // TODO: better way to get the offset top?
        let node = this.refs.node
        if (!node) {
          return
        }
        mark('calculateListOffset')
        let { offsetParent } = node
        // TODO: offsetParent is null sometimes in testcafe tests
        let listOffset = offsetParent ? offsetParent.offsetTop : 0
        this.store.setForRealm({ listOffset })
        stop('calculateListOffset')
      }
    }
  }
</script>


code:
<slot></slot>
<script>
  import { virtualListStore } from './virtualListStore'
  import throttle from 'lodash-es/throttle'
  import { isFullscreen, attachFullscreenListener, detachFullscreenListener } from '../../_utils/fullscreen'
  import { mark, stop } from '../../_utils/marks'
  import { doubleRAF } from '../../_utils/doubleRAF'
  import { observe } from 'svelte-extras'
  import {
    addScrollListener,
    removeScrollListener,
    getScrollContainer,
    getOffsetHeight
  } from '../../_utils/scrollContainer'
  import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'

  const SCROLL_EVENT_DELAY = 300

  export default {
    oncreate () {
      mark('onCreate VirtualListContainer')
      let {
        realm
      } = this.get()
      this.store.setCurrentRealm(realm)
      this.setupScroll()
      this.setupFullscreen()
      this.onResize = this.onResize.bind(this)
      let { scrollTop } = this.store.get()
      let scrollContainer = getScrollContainer()
      if (scrollTop > 0) {
        this.observe('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight => {
          console.log('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight)
          let { initializedScrollTop } = this.get()
          if (!initializedScrollTop && allVisibleItemsHaveHeight) {
            this.set({ 'initializedScrollTop': true })
            requestAnimationFrame(() => {
              mark('set scrollTop')
              console.log('forcing scroll top to ', scrollTop)
              scrollContainer.scrollTop = scrollTop
              stop('set scrollTop')
              doubleRAF(() => {
                console.log('initialized VirtualList')
                this.fire('initialized')
              })
            })
          }
        })
      } else {
        this.fire('noNeedToScroll')
        this.observe('allVisibleItemsHaveHeight', allVisibleItemsHaveHeight => {
          if (allVisibleItemsHaveHeight) {
            console.log('initialized VirtualList')
            this.fire('initialized')
          }
        })
        this.onResize()
      }
      registerResizeListener(this.onResize)
      stop('onCreate VirtualListContainer')
    },
    ondestroy () {
      this.teardownScroll()
      this.teardownFullscreen()
      this.store.setCurrentRealm(null)
      unregisterResizeListener(this.onResize)
    },
    store: () => virtualListStore,
    methods: {
      observe,
      setupScroll () {
        this.scrollListener = throttle(event => {
          let { fullscreen } = this.get()
          if (fullscreen) {
            return
          }
          this.onScroll()
        }, SCROLL_EVENT_DELAY, {
          leading: true,
          trailing: true
        })
        addScrollListener(this.scrollListener)
      },
      teardownScroll () {
        removeScrollListener(this.scrollListener)
      },
      setupFullscreen () {
        this.onFullscreenChange = this.onFullscreenChange.bind(this)
        attachFullscreenListener(this.onFullscreenChange)
      },
      teardownFullscreen () {
        detachFullscreenListener(this.onFullscreenChange)
      },
      onScroll () {
        let { scrollTop, scrollHeight } = getScrollContainer()

        doubleRAF(() => {
          mark('onScroll -> setForRealm()')
          this.store.setForRealm({ scrollTop, scrollHeight })
          stop('onScroll -> setForRealm()')
        })
      },
      onFullscreenChange () {
        mark('onFullscreenChange')
        console.log('is fullscreen? ', isFullscreen())
        this.set({ fullscreen: isFullscreen() })
        stop('onFullscreenChange')
      },
      onResize () {
        this.store.setForRealm({
          scrollHeight: getScrollContainer().scrollHeight,
          offsetHeight: getOffsetHeight()
        })
      }
    },
    computed: {
      // TODO: bug in svelte/store – the observer in oncreate() never get removed without this hack
      allVisibleItemsHaveHeight: ({ $allVisibleItemsHaveHeight }) => $allVisibleItemsHaveHeight
    }
  }
</script>


code:
<div class="virtual-list-footer"
  ref:node
  style="transform: translateY({$heightWithoutFooter}px);" >
  <svelte:component this={component} />
</div>
<style>
  .virtual-list-footer {
    position: absolute;
    top: 0;
    width: 100%;
  }
</style>
<script>
  import { virtualListStore } from './virtualListStore'
  import { mark, stop } from '../../_utils/marks'

  export default {
    oncreate () {
      requestAnimationFrame(() => {
        let node = this.refs.node
        if (!node) {
          return
        }

        mark('VirtualListFooter gBCR')
        let rect = node.getBoundingClientRect()
        stop('VirtualListFooter gBCR')
        this.store.setForRealm({ footerHeight: rect.height })
      })
    },
    store: () => virtualListStore
  }
</script>


code:
<div class="virtual-list-header {shown ? 'shown' : ''} {fadedIn ? 'faded-in' : ''}"
     ref:node >
  <svelte:component this={component} {virtualProps} />
</div>
<style>
  .virtual-list-header {
    position: absolute;
    top: 0;
    width: 100%;
    opacity: 0;
    z-index: 10;
    transition: none;
    display: none;
  }
  .virtual-list-header.shown {
    display: block;
    transition: opacity 0.333s linear;
  }
  .virtual-list-header.faded-in {
    opacity: 1;
  }
</style>
<script>
  import { virtualListStore } from './virtualListStore'
  import { doubleRAF } from '../../_utils/doubleRAF'
  import { mark, stop } from '../../_utils/marks'
  import { observe } from 'svelte-extras'

  export default {
    oncreate () {
      this.observe('shown', shown => {
        if (shown) {
          this.doCalculateHeight()
          doubleRAF(() => this.set({ fadedIn: true })) //  animate in
        } else {
          this.set({ fadedIn: false })
        }
      }, { init: false })
    },
    data: () => ({
      fadedIn: false
    }),
    store: () => virtualListStore,
    methods: {
      observe,
      doCalculateHeight () {
        let { heightCalculated } = this.get()
        if (heightCalculated) { // only need to calculate once, it never changes
          return
        }
        this.set({ heightCalculated: true })
        requestAnimationFrame(() => {
          mark('VirtualListHeader gBCR')
          let rect = this.refs.node.getBoundingClientRect()
          stop('VirtualListHeader gBCR')
          this.store.setForRealm({ headerHeight: rect.height })
        })
      }
    }
  }
</script>


code:
<div class="virtual-list-item list-item {shown ? 'shown' : ''}"
     aria-hidden={!shown}
     ref:node
     style="transform: translateY({offset}px);" >
  <svelte:component this={component}
              virtualProps={props}
              virtualIndex={index}
              virtualLength={$length}
              virtualKey={key}
              on:recalculateHeight="doRecalculateHeight()"/>
</div>
<style>
  .virtual-list-item {
    position: absolute;
    width: 100%;
    top: 0;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.333s linear;
    contain: content; /* see https://www.w3.org/TR/2018/CR-css-contain-1-20181108/#valdef-contain-content */
  }
  .virtual-list-item.shown {
    opacity: 1;
    pointer-events: auto;
  }
</style>
<script>
  import { virtualListStore } from './virtualListStore'
  import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'
  import { mark, stop } from '../../_utils/marks'

  export default {
    oncreate () {
      let { key } = this.get()
      let node = this.refs.node
      requestAnimationFrame(() => {
        if (!node || !key) {
          return
        }
        mark('VirtualListItem gBCR')
        let rect = node.getBoundingClientRect()
        stop('VirtualListItem gBCR')
        // update all item heights in one batch for better perf
        this.store.batchUpdateForRealm('itemHeights', key, rect.height)
      })
      this.doRecalculateHeight = this.doRecalculateHeight.bind(this)
      registerResizeListener(this.doRecalculateHeight)
    },
    ondestroy () {
      unregisterResizeListener(this.doRecalculateHeight)
    },
    store: () => virtualListStore,
    computed: {
      'shown': ({ $itemHeights, key }) => $itemHeights[key] > 0
    },
    methods: {
      doRecalculateHeight () {
        // Recalculate immediately because this is done on-demand, e.g.
        // when clicking the "More" button on a spoiler.
        let rect = this.refs.node.getBoundingClientRect()
        let { key } = this.get()
        let { itemHeights } = this.store.get()
        itemHeights[key] = rect.height
        this.store.setForRealm({ itemHeights })
      }
    }
  }
</script>


code:
{#if props}
  <VirtualListItem {component}
                   {offset}
                   {props}
                   {key}
                   {index}
  />
{/if}
<script>
  import VirtualListItem from './VirtualListItem'
  import { mark, stop } from '../../_utils/marks'

  export default {
    async oncreate () {
      let { makeProps, key } = this.get()
      if (makeProps) {
        let props = await makeProps(key)
        mark('VirtualListLazyItem set props')
        this.set({ props: props })
        stop('VirtualListLazyItem set props')
      }
    },
    data: () => ({
      props: void 0
    }),
    components: {
      VirtualListItem
    }
  }
</script>


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

const RENDER_BUFFER_FACTOR = 2.5

class VirtualListStore extends RealmStore {
  constructor (state) {
    super(state, /* maxSize */ 10)
  }
}

const virtualListStore = new VirtualListStore()

virtualListStore.computeForRealm('items', null)
virtualListStore.computeForRealm('showFooter', false)
virtualListStore.computeForRealm('footerHeight', 0)
virtualListStore.computeForRealm('showHeader', false)
virtualListStore.computeForRealm('headerHeight', 0)
virtualListStore.computeForRealm('scrollTop', 0)
virtualListStore.computeForRealm('scrollHeight', 0)
virtualListStore.computeForRealm('offsetHeight', 0)
virtualListStore.computeForRealm('listOffset', 0)
virtualListStore.computeForRealm('itemHeights', {})

virtualListStore.compute('rawVisibleItems',
  ['items', 'scrollTop', 'itemHeights', 'offsetHeight', 'showHeader', 'headerHeight', 'listOffset'],
  (items, scrollTop, itemHeights, offsetHeight, showHeader, headerHeight, listOffset) => {
    if (process.browser && process.env.NODE_ENV !== 'production') {
      window.rawVisibleItemsComputed = (window.rawVisibleItemsComputed || 0) + 1
    }
    mark('compute visibleItems')
    if (!items) {
      return null
    }
    let effectiveScrollTop = scrollTop - listOffset
    let renderBuffer = RENDER_BUFFER_FACTOR * offsetHeight
    let visibleItems = []
    let totalOffset = showHeader ? headerHeight : 0
    let len = items.length
    let i = -1
    while (++i < len) {
      let key = items[i]
      let height = itemHeights[key] || 0
      let currentOffset = totalOffset
      totalOffset += height
      let isAboveViewport = (currentOffset < effectiveScrollTop)
      if (isAboveViewport) {
        if ((effectiveScrollTop - height - renderBuffer) > currentOffset) {
          continue // above the area we want to render
        }
      } else {
        if (currentOffset > (effectiveScrollTop + offsetHeight + renderBuffer)) {
          break // below the area we want to render
        }
      }
      visibleItems.push({
        offset: currentOffset,
        key: key,
        index: i
      })
    }
    stop('compute visibleItems')
    return visibleItems
  })

reselect(virtualListStore, 'visibleItems', 'rawVisibleItems')

virtualListStore.compute('heightWithoutFooter',
  ['items', 'itemHeights', 'showHeader', 'headerHeight'],
  (items, itemHeights, showHeader, headerHeight) => {
    if (!items) {
      return 0
    }
    let sum = showHeader ? headerHeight : 0
    let i = -1
    let len = items.length
    while (++i < len) {
      sum += itemHeights[items[i]] || 0
    }
    return sum
  })

virtualListStore.compute('height',
  ['heightWithoutFooter', 'showFooter', 'footerHeight'],
  (heightWithoutFooter, showFooter, footerHeight) => {
    return showFooter ? (heightWithoutFooter + footerHeight) : heightWithoutFooter
  })

virtualListStore.compute('length', ['items'], (items) => items ? items.length : 0)

virtualListStore.compute('allVisibleItemsHaveHeight',
  ['visibleItems', 'itemHeights'],
  (visibleItems, itemHeights) => {
    if (!visibleItems) {
      return false
    }
    for (let visibleItem of visibleItems) {
      if (!itemHeights[visibleItem.key]) {
        return false
      }
    }
    return true
  })

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

  virtualListStore.on('state', ({ changed }) => {
    if (changed.visibleItems) {
      window.visibleItemsChangedCount = (window.visibleItemsChangedCount || 0) + 1
    }
    if (changed.rawVisibleItems) {
      window.rawVisibleItemsChangedCount = (window.rawVisibleItemsChangedCount || 0) + 1
    }
  })
}

export {
  virtualListStore
}


code:
import { ACCOUNTS_STORE, USERNAME_LOWERCASE } from './constants'
import { accountsCache } from './cache'
import { cloneForStorage, getGenericEntityWithId, setGenericEntityWithId } from './helpers'
import { dbPromise, getDatabase } from './databaseLifecycle'
import { createAccountUsernamePrefixKeyRange } from './keys'

export async function getAccount (instanceName, accountId) {
  return getGenericEntityWithId(ACCOUNTS_STORE, accountsCache, instanceName, accountId)
}

export async function setAccount (instanceName, account) {
  return setGenericEntityWithId(ACCOUNTS_STORE, accountsCache, instanceName, cloneForStorage(account))
}

export async function searchAccountsByUsername (instanceName, usernamePrefix, limit) {
  limit = limit || 20
  const db = await getDatabase(instanceName)
  return dbPromise(db, ACCOUNTS_STORE, 'readonly', (accountsStore, callback) => {
    let keyRange = createAccountUsernamePrefixKeyRange(usernamePrefix.toLowerCase())
    accountsStore.index(USERNAME_LOWERCASE).getAll(keyRange, limit).onsuccess = e => {
      callback(e.target.result)
    }
  })
}


code:
// All database functions are asynchronous, so we can just proxy here and
// put an async import of the database, to avoid including it in the main bundle
// (which doesn't need to run when the user isn't logged in).

import { importDatabase } from '../_utils/asyncModules'

const handler = {
  get: function (obj, prop) {
    return async function (...args) {
      if (!obj[prop]) {
        let database = await importDatabase()
        obj[prop] = database[prop]
      }
      return obj[prop].apply(null, args)
    }
  }
}

export const asyncDatabase = new Proxy({}, handler)


code:
import QuickLRU from 'quick-lru'

export const statusesCache = {
  maxSize: 100,
  caches: {}
}
export const accountsCache = {
  maxSize: 50,
  caches: {}
}
export const relationshipsCache = {
  maxSize: 20,
  caches: {}
}
export const metaCache = {
  maxSize: 20,
  caches: {}
}
export const notificationsCache = {
  maxSize: 50,
  caches: {}
}

if (process.browser && process.env.NODE_ENV !== 'production') {
  (typeof self !== 'undefined' ? self : window).cacheStats = {
    statuses: statusesCache,
    accounts: accountsCache,
    relationships: relationshipsCache,
    meta: metaCache,
    notifications: notificationsCache
  }
}

function getOrCreateInstanceCache (cache, instanceName) {
  let cached = cache.caches[instanceName]
  if (!cached) {
    cached = cache.caches[instanceName] = new QuickLRU({ maxSize: cache.maxSize })
  }
  return cached
}

export function clearCache (cache, instanceName) {
  delete cache.caches[instanceName]
}
export function clearAllCaches (instanceName) {
  let allCaches = [statusesCache, accountsCache, relationshipsCache, metaCache, notificationsCache]
  for (let cache of allCaches) {
    clearCache(cache, instanceName)
  }
}
export function setInCache (cache, instanceName, key, value) {
  let instanceCache = getOrCreateInstanceCache(cache, instanceName)
  return instanceCache.set(key, value)
}

export function getInCache (cache, instanceName, key) {
  let instanceCache = getOrCreateInstanceCache(cache, instanceName)
  return instanceCache.get(key)
}

export function hasInCache (cache, instanceName, key) {
  let instanceCache = getOrCreateInstanceCache(cache, instanceName)
  let res = instanceCache.has(key)
  if (process.env.NODE_ENV !== 'production') {
    if (res) {
      cache.hits = (cache.hits || 0) + 1
    } else {
      cache.misses = (cache.misses || 0) + 1
    }
  }
  return res
}

export function deleteFromCache (cache, instanceName, key) {
  let instanceCache = getOrCreateInstanceCache(cache, instanceName)
  instanceCache.delete(key)
}


code:
import { dbPromise, getDatabase } from './databaseLifecycle'
import {
  ACCOUNTS_STORE,
  NOTIFICATION_TIMELINES_STORE,
  NOTIFICATIONS_STORE,
  PINNED_STATUSES_STORE,
  RELATIONSHIPS_STORE,
  STATUS_TIMELINES_STORE,
  STATUSES_STORE,
  THREADS_STORE,
  TIMESTAMP
} from './constants'
import debounce from 'lodash-es/debounce'
import { mark, stop } from '../_utils/marks'
import { deleteAll } from './utils'
import { createPinnedStatusKeyRange, createThreadKeyRange } from './keys'
import { getKnownInstances } from './knownInstances'
import noop from 'lodash-es/noop'

const BATCH_SIZE = 20
export const TIME_AGO = 5 * 24 * 60 * 60 * 1000 // five days ago
const DELAY = 5 * 60 * 1000 // five minutes

function batchedGetAll (callGetAll, callback) {
  function nextBatch () {
    callGetAll().onsuccess = function (e) {
      let results = e.target.result
      callback(results)
      if (results.length) {
        nextBatch()
      }
    }
  }
  nextBatch()
}

function cleanupStatuses (statusesStore, statusTimelinesStore, threadsStore, cutoff) {
  batchedGetAll(
    () => statusesStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
    results => {
      results.forEach(statusId => {
        statusesStore.delete(statusId)
        deleteAll(
          statusTimelinesStore,
          statusTimelinesStore.index('statusId'),
          IDBKeyRange.only(statusId)
        )
        deleteAll(
          threadsStore,
          threadsStore,
          createThreadKeyRange(statusId)
        )
      })
    }
  )
}

function cleanupNotifications (notificationsStore, notificationTimelinesStore, cutoff) {
  batchedGetAll(
    () => notificationsStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
    results => {
      results.forEach(notificationId => {
        notificationsStore.delete(notificationId)
        deleteAll(
          notificationTimelinesStore,
          notificationTimelinesStore.index('notificationId'),
          IDBKeyRange.only(notificationId)
        )
      })
    }
  )
}

function cleanupAccounts (accountsStore, pinnedStatusesStore, cutoff) {
  batchedGetAll(
    () => accountsStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
    results => {
      results.forEach(accountId => {
        accountsStore.delete(accountId)
        deleteAll(
          pinnedStatusesStore,
          pinnedStatusesStore,
          createPinnedStatusKeyRange(accountId)
        )
      })
    }
  )
}

function cleanupRelationships (relationshipsStore, cutoff) {
  batchedGetAll(
    () => relationshipsStore.index(TIMESTAMP).getAllKeys(IDBKeyRange.upperBound(cutoff), BATCH_SIZE),
    results => {
      results.forEach(relationshipId => {
        relationshipsStore.delete(relationshipId)
      })
    }
  )
}

export async function cleanup (instanceName) {
  console.log('cleanup', instanceName)
  mark(`cleanup:${instanceName}`)
  let db = await getDatabase(instanceName)
  let storeNames = [
    STATUSES_STORE,
    STATUS_TIMELINES_STORE,
    NOTIFICATIONS_STORE,
    NOTIFICATION_TIMELINES_STORE,
    ACCOUNTS_STORE,
    RELATIONSHIPS_STORE,
    THREADS_STORE,
    PINNED_STATUSES_STORE
  ]
  await dbPromise(db, storeNames, 'readwrite', (stores) => {
    let [
      statusesStore,
      statusTimelinesStore,
      notificationsStore,
      notificationTimelinesStore,
      accountsStore,
      relationshipsStore,
      threadsStore,
      pinnedStatusesStore
    ] = stores

    let cutoff = Date.now() - TIME_AGO

    cleanupStatuses(statusesStore, statusTimelinesStore, threadsStore, cutoff)
    cleanupNotifications(notificationsStore, notificationTimelinesStore, cutoff)
    cleanupAccounts(accountsStore, pinnedStatusesStore, cutoff)
    cleanupRelationships(relationshipsStore, cutoff)
  })
  stop(`cleanup:${instanceName}`)
}

function doCleanup (instanceName) {
  // run in setTimeout because we're in a worker and there's no requestIdleCallback
  setTimeout(() => cleanup(instanceName))
}

async function scheduledCleanup () {
  console.log('scheduledCleanup')
  let knownInstances = await getKnownInstances()
  for (let instance of knownInstances) {
    doCleanup(instance)
  }
}

// we have unit tests that test indexedDB; we don't want this thing to run forever
export const scheduleCleanup = process.browser ? debounce(scheduledCleanup, DELAY) : noop


code:
import { accountsCache, clearCache, metaCache, statusesCache } from './cache'
import { deleteDatabase } from './databaseLifecycle'

export async function clearDatabaseForInstance (instanceName) {
  clearCache(statusesCache, instanceName)
  clearCache(accountsCache, instanceName)
  clearCache(metaCache, instanceName)
  await deleteDatabase(instanceName)
}


code:
export const STATUSES_STORE = 'statuses-v4'
export const STATUS_TIMELINES_STORE = 'status_timelines-v4'
export const META_STORE = 'meta-v4'
export const ACCOUNTS_STORE = 'accounts-v4'
export const RELATIONSHIPS_STORE = 'relationships-v4'
export const NOTIFICATIONS_STORE = 'notifications-v4'
export const NOTIFICATION_TIMELINES_STORE = 'notification_timelines-v4'
export const PINNED_STATUSES_STORE = 'pinned_statuses-v4'
export const THREADS_STORE = 'threads-v4'

export const TIMESTAMP = '__pinafore_ts'
export const ACCOUNT_ID = '__pinafore_acct_id'
export const STATUS_ID = '__pinafore_status_id'
export const REBLOG_ID = '__pinafore_reblog_id'
export const USERNAME_LOWERCASE = '__pinafore_acct_lc'

export const DB_VERSION_INITIAL = 9
export const DB_VERSION_SEARCH_ACCOUNTS = 10
export const DB_VERSION_SNOWFLAKE_IDS = 12 // 11 skipped because of mistake deployed to dev.pinafore.social

// Using an object for these so that unit tests can change them
export const DB_VERSION_CURRENT = { version: 12 }
export const CURRENT_TIME = { now: () => Date.now() }


code:
// The proxy stuff doesn't play well with IDEs, so use this as
// the dev mode database.js, but swap out the other one in Webpack.

import * as database from './databaseApis'
export { database }


code:
import { asyncDatabase } from './asyncDatabase'
export { asyncDatabase as database }


code:
export * from './accounts'
export * from './clear'
export * from './timelines/lookup'
export * from './timelines/pinnedStatuses'
export * from './timelines/pagination'
export * from './timelines/getStatusOrNotification'
export * from './timelines/updateStatus'
export * from './timelines/deletion'
export * from './timelines/insertion'
export * from './meta'
export * from './relationships'


code:
import { DB_VERSION_CURRENT } from './constants'
import { addKnownInstance, deleteKnownInstance } from './knownInstances'
import { migrations } from './migrations'
import { clearAllCaches } from './cache'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'

const openReqs = {}
const databaseCache = {}

function createDatabase (instanceName) {
  return new Promise((resolve, reject) => {
    let req = indexedDB.open(instanceName, DB_VERSION_CURRENT.version)
    openReqs[instanceName] = req
    req.onerror = reject
    req.onblocked = () => {
      console.error('idb blocked')
    }
    req.onupgradeneeded = (e) => {
      let db = req.result
      let tx = e.currentTarget.transaction

      let migrationsToDo = migrations.filter(({ version }) => e.oldVersion < version)

      function doNextMigration () {
        if (!migrationsToDo.length) {
          return
        }
        let { migration } = migrationsToDo.shift()
        migration(db, tx, doNextMigration)
      }
      doNextMigration()
    }
    req.onsuccess = () => resolve(req.result)
  })
}

export async function getDatabase (instanceName) {
  if (!instanceName) {
    throw new Error('instanceName is undefined in getDatabase()')
  }
  if (!databaseCache[instanceName]) {
    databaseCache[instanceName] = await createDatabase(instanceName)
    await addKnownInstance(instanceName)
  }
  return databaseCache[instanceName]
}

export async function dbPromise (db, storeName, readOnlyOrReadWrite, cb) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, readOnlyOrReadWrite)
    let store = typeof storeName === 'string'
      ? tx.objectStore(storeName)
      : storeName.map(name => tx.objectStore(name))
    let res
    cb(store, (result) => {
      res = result
    })

    tx.oncomplete = () => resolve(res)
    tx.onerror = () => reject(tx.error)
  })
}

export function deleteDatabase (instanceName) {
  return new Promise((resolve, reject) => {
    // close any open requests
    let openReq = openReqs[instanceName]
    if (openReq && openReq.result) {
      openReq.result.close()
    }
    delete openReqs[instanceName]
    delete databaseCache[instanceName]
    let req = indexedDB.deleteDatabase(instanceName)
    req.onsuccess = () => resolve()
    req.onerror = () => reject(req.error)
    req.onblocked = () => console.error(`database ${instanceName} blocked`)
  }).then(() => deleteKnownInstance(instanceName))
    .then(() => clearAllCaches(instanceName))
}

export function closeDatabase (instanceName) {
  // close any open requests
  let openReq = openReqs[instanceName]
  if (openReq && openReq.result) {
    openReq.result.close()
  }
  delete openReqs[instanceName]
  delete databaseCache[instanceName]
  clearAllCaches(instanceName)
}

if (process.browser) {
  lifecycle.addEventListener('statechange', event => {
    if (event.newState === 'frozen') { // page is frozen, close IDB connections
      Object.keys(openReqs).forEach(instanceName => {
        closeDatabase(instanceName)
        console.log('closed instance DBs')
      })
    }
  })
}


code:
import { dbPromise, getDatabase } from './databaseLifecycle'
import { getInCache, hasInCache, setInCache } from './cache'
import {
  ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE, CURRENT_TIME
} from './constants'

export async function getGenericEntityWithId (store, cache, instanceName, id) {
  if (hasInCache(cache, instanceName, id)) {
    return getInCache(cache, instanceName, id)
  }
  const db = await getDatabase(instanceName)
  let result = await dbPromise(db, store, 'readonly', (store, callback) => {
    store.get(id).onsuccess = (e) => callback(e.target.result)
  })
  setInCache(cache, instanceName, id, result)
  return result
}

export async function setGenericEntityWithId (store, cache, instanceName, entity) {
  setInCache(cache, instanceName, entity.id, entity)
  const db = await getDatabase(instanceName)
  return dbPromise(db, store, 'readwrite', (store) => {
    store.put(entity)
  })
}

export function cloneForStorage (obj) {
  let res = {}
  let keys = Object.keys(obj)
  for (let key of keys) {
    let value = obj[key]
    // save storage space by skipping nulls, 0s, falses, empty strings, and empty arrays
    if (!value || (Array.isArray(value) && value.length === 0)) {
      continue
    }
    switch (key) {
      case 'account':
        res[ACCOUNT_ID] = value.id
        break
      case 'status':
        res[STATUS_ID] = value.id
        break
      case 'reblog':
        res[REBLOG_ID] = value.id
        break
      case 'acct':
        res[key] = value
        res[USERNAME_LOWERCASE] = value.toLowerCase()
        break
      default:
        res[key] = value
        break
    }
  }
  res[TIMESTAMP] = CURRENT_TIME.now()
  return res
}


code:
import { toReversePaddedBigInt, zeroPad } from '../_utils/statusIdSorting'

//
// timelines
//

export function createTimelineId (timeline, id) {
  // reverse chronological order, prefixed by timeline
  return timeline + '\u0000' + toReversePaddedBigInt(id)
}

export function createTimelineKeyRange (timeline, maxId) {
  let negBigInt = maxId && toReversePaddedBigInt(maxId)
  let start = negBigInt ? (timeline + '\u0000' + negBigInt) : (timeline + '\u0000')
  let end = timeline + '\u0000\uffff'
  return IDBKeyRange.bound(start, end, true, true)
}

//
// threads
//

export function createThreadId (statusId, i) {
  return statusId + '\u0000' + zeroPad(i, 5)
}

export function createThreadKeyRange (statusId) {
  return IDBKeyRange.bound(
    statusId + '\u0000',
    statusId + '\u0000\uffff'
  )
}

//
// pinned statues
//

export function createPinnedStatusId (accountId, i) {
  return accountId + '\u0000' + zeroPad(i, 3)
}

export function createPinnedStatusKeyRange (accountId) {
  return IDBKeyRange.bound(
    accountId + '\u0000',
    accountId + '\u0000\uffff'
  )
}

//
// accounts
//

export function createAccountUsernamePrefixKeyRange (accountUsernamePrefix) {
  return IDBKeyRange.bound(
    accountUsernamePrefix,
    accountUsernamePrefix + '\uffff'
  )
}


code:
import { set, keys, del, close } from '../_thirdparty/idb-keyval/idb-keyval'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'

const PREFIX = 'known-instance-'

export async function getKnownInstances () {
  return (await keys())
    .filter(_ => _.startsWith(PREFIX))
    .map(_ => _.substring(PREFIX.length))
}

export async function addKnownInstance (instanceName) {
  return set(PREFIX + instanceName, true)
}

export async function deleteKnownInstance (instanceName) {
  return del(PREFIX + instanceName)
}

if (process.browser) {
  lifecycle.addEventListener('statechange', async event => {
    if (event.newState === 'frozen') { // page is frozen, close IDB connections
      await close()
      console.log('closed knownInstances DB')
    }
  })
}


code:
import { dbPromise, getDatabase } from './databaseLifecycle'
import { META_STORE } from './constants'
import { metaCache, hasInCache, getInCache, setInCache } from './cache'

async function getMetaProperty (instanceName, key) {
  if (hasInCache(metaCache, instanceName, key)) {
    return getInCache(metaCache, instanceName, key)
  }
  const db = await getDatabase(instanceName)
  let result = await dbPromise(db, META_STORE, 'readonly', (store, callback) => {
    store.get(key).onsuccess = (e) => {
      callback(e.target.result)
    }
  })
  setInCache(metaCache, instanceName, key, result)
  return result
}

async function setMetaProperty (instanceName, key, value) {
  setInCache(metaCache, instanceName, key, value)
  const db = await getDatabase(instanceName)
  return dbPromise(db, META_STORE, 'readwrite', (store) => {
    store.put(value, key)
  })
}

export async function getInstanceVerifyCredentials (instanceName) {
  return getMetaProperty(instanceName, 'verifyCredentials')
}

export async function setInstanceVerifyCredentials (instanceName, value) {
  return setMetaProperty(instanceName, 'verifyCredentials', value)
}

export async function getInstanceInfo (instanceName) {
  return getMetaProperty(instanceName, 'instance')
}

export async function setInstanceInfo (instanceName, value) {
  return setMetaProperty(instanceName, 'instance', value)
}

export async function getLists (instanceName) {
  return getMetaProperty(instanceName, 'lists')
}

export async function setLists (instanceName, value) {
  return setMetaProperty(instanceName, 'lists', value)
}

export async function getCustomEmoji (instanceName) {
  return getMetaProperty(instanceName, 'customEmoji')
}

export async function setCustomEmoji (instanceName, value) {
  return setMetaProperty(instanceName, 'customEmoji', value)
}


code:
import {
  ACCOUNTS_STORE, DB_VERSION_SNOWFLAKE_IDS, DB_VERSION_INITIAL,
  DB_VERSION_SEARCH_ACCOUNTS, META_STORE,
  NOTIFICATION_TIMELINES_STORE,
  NOTIFICATIONS_STORE, PINNED_STATUSES_STORE,
  REBLOG_ID, RELATIONSHIPS_STORE,
  STATUS_ID,
  STATUS_TIMELINES_STORE,
  STATUSES_STORE, THREADS_STORE,
  TIMESTAMP, USERNAME_LOWERCASE
} from './constants'
import { toReversePaddedBigInt } from '../_utils/statusIdSorting'

function initialMigration (db, tx, done) {
  function createObjectStore (name, init, indexes) {
    let store = init
      ? db.createObjectStore(name, init)
      : db.createObjectStore(name)
    if (indexes) {
      Object.keys(indexes).forEach(indexKey => {
        store.createIndex(indexKey, indexes[indexKey])
      })
    }
  }

  createObjectStore(STATUSES_STORE, { keyPath: 'id' }, {
    [TIMESTAMP]: TIMESTAMP,
    [REBLOG_ID]: REBLOG_ID
  })
  createObjectStore(STATUS_TIMELINES_STORE, null, {
    'statusId': ''
  })
  createObjectStore(NOTIFICATIONS_STORE, { keyPath: 'id' }, {
    [TIMESTAMP]: TIMESTAMP,
    [STATUS_ID]: STATUS_ID
  })
  createObjectStore(NOTIFICATION_TIMELINES_STORE, null, {
    'notificationId': ''
  })
  createObjectStore(ACCOUNTS_STORE, { keyPath: 'id' }, {
    [TIMESTAMP]: TIMESTAMP
  })
  createObjectStore(RELATIONSHIPS_STORE, { keyPath: 'id' }, {
    [TIMESTAMP]: TIMESTAMP
  })
  createObjectStore(THREADS_STORE, null, {
    'statusId': ''
  })
  createObjectStore(PINNED_STATUSES_STORE, null, {
    'statusId': ''
  })
  createObjectStore(META_STORE)
  done()
}

function addSearchAccountsMigration (db, tx, done) {
  tx.objectStore(ACCOUNTS_STORE)
    .createIndex(USERNAME_LOWERCASE, USERNAME_LOWERCASE)
  done()
}

function snowflakeIdsMigration (db, tx, done) {
  let stores = [STATUS_TIMELINES_STORE, NOTIFICATION_TIMELINES_STORE]
  let storeDoneCount = 0

  // Here we have to convert the old "reversePaddedBigInt" format to the new
  // one which is compatible with Pleroma-style snowflake IDs.
  stores.forEach(store => {
    let objectStore = tx.objectStore(store)
    let cursor = objectStore.openCursor()
    cursor.onsuccess = e => {
      let { result } = e.target
      if (result) {
        let { key, value } = result
        // key is timeline name plus delimiter plus reverse padded big int
        let newKey = key.split('\u0000')[0] + '\u0000' + toReversePaddedBigInt(value)

        objectStore.delete(key).onsuccess = () => {
          objectStore.add(value, newKey).onsuccess = () => {
            result.continue()
          }
        }
      } else {
        if (++storeDoneCount === stores.length) {
          done()
        }
      }
    }
  })
}

export const migrations = [
  {
    version: DB_VERSION_INITIAL,
    migration: initialMigration
  },
  {
    version: DB_VERSION_SEARCH_ACCOUNTS,
    migration: addSearchAccountsMigration
  },
  {
    version: DB_VERSION_SNOWFLAKE_IDS,
    migration: snowflakeIdsMigration
  }
]


code:
import { cloneForStorage, getGenericEntityWithId, setGenericEntityWithId } from './helpers'
import { RELATIONSHIPS_STORE } from './constants'
import { relationshipsCache } from './cache'

export async function getRelationship (instanceName, accountId) {
  return getGenericEntityWithId(RELATIONSHIPS_STORE, relationshipsCache, instanceName, accountId)
}

export async function setRelationship (instanceName, relationship) {
  return setGenericEntityWithId(RELATIONSHIPS_STORE, relationshipsCache, instanceName, cloneForStorage(relationship))
}


code:
import {
  accountsCache, setInCache, statusesCache
} from '../cache'

export function cacheStatus (status, instanceName) {
  setInCache(statusesCache, instanceName, status.id, status)
  setInCache(accountsCache, instanceName, status.account.id, status.account)
  if (status.reblog) {
    setInCache(accountsCache, instanceName, status.reblog.account.id, status.reblog.account)
  }
}