From a79bad5cdb896fd965e544df3ba8d0a87b3db458 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 19 Jun 2020 12:46:48 +0200
Subject: StatusContent: Better separate subject from status content.

---
 src/components/status_content/status_content.vue | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index efc2485e..df980a71 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -31,7 +31,7 @@
       <div
         class="status-content media-body"
         @click.prevent="linkClicked"
-        v-html="contentHtml"
+        v-html="postBodyHtml"
       />
       <a
         v-if="showingLongSubject"
@@ -53,16 +53,16 @@
         @click.prevent="toggleShowMore"
       >{{ $t("general.show_more") }}</a>
       <div
-        v-if="!hideSubjectStatus"
-        class="status-content media-body"
+        v-if="status.summary_html"
+        class="status-content media-body summary"
         @click.prevent="linkClicked"
-        v-html="contentHtml"
+        v-html="status.summary_html"
       />
       <div
-        v-else
+        v-if="!hideSubjectStatus"
         class="status-content media-body"
         @click.prevent="linkClicked"
-        v-html="status.summary_html"
+        v-html="postBodyHtml"
       />
       <a
         v-if="hideSubjectStatus"
@@ -181,6 +181,10 @@ $status-margin: 0.75em;
     line-height: 1.4em;
     white-space: pre-wrap;
 
+    &.summary {
+      font-weight: bold;
+    }
+
     blockquote {
       margin: 0.2em 0 0.2em 2em;
       font-style: italic;
@@ -226,7 +230,7 @@ $status-margin: 0.75em;
 
 .greentext {
   color: $fallback--cGreen;
-  color: var(--postGreentext, $fallback--cGreen);
+  color: var(--cGreen, $fallback--cGreen);
 }
 
 .timeline :not(.panel-disabled) > {
-- 
cgit v1.2.3-70-g09d2


From 4da0a0c0bfcfaccd58def8d2170b952c1d15106a Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 19 Jun 2020 12:49:42 +0200
Subject: StatusContent: Fix greentext.

---
 src/components/status_content/status_content.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index df980a71..431661eb 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -230,7 +230,7 @@ $status-margin: 0.75em;
 
 .greentext {
   color: $fallback--cGreen;
-  color: var(--cGreen, $fallback--cGreen);
+  color: var(--postGreentext, $fallback--cGreen);
 }
 
 .timeline :not(.panel-disabled) > {
-- 
cgit v1.2.3-70-g09d2


From 44edb730c1e2298a00be0c1a139f80a1335ad7cf Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Fri, 26 Jun 2020 14:07:39 +0300
Subject: rip restyle subject, fix some issues with long subject

---
 src/components/status_content/status_content.js  |   2 +-
 src/components/status_content/status_content.vue | 115 ++++++++++++++---------
 2 files changed, 74 insertions(+), 43 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index cfee77a3..66501b3e 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -44,7 +44,7 @@ const StatusContent = {
       return lengthScore > 20
     },
     longSubject () {
-      return this.status.summary.length > 900
+      return this.status.summary.length > 240
     },
     // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
     mightHideBecauseSubject () {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 431661eb..5698b4c0 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -3,45 +3,32 @@
   <div class="status-body">
     <slot name="header" />
     <div
-      v-if="longSubject"
-      class="status-content-wrapper"
-      :class="{ 'tall-status': !showingLongSubject }"
+      v-if="status.summary_html"
+      class="summary-wrapper"
+      :class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
     >
-      <a
-        v-if="!showingLongSubject"
-        class="tall-status-hider"
-        :class="{ 'tall-status-hider_focused': focused }"
-        href="#"
-        @click.prevent="showingLongSubject=true"
-      >
-        {{ $t("general.show_more") }}
-        <span
-          v-if="hasImageAttachments"
-          class="icon-picture"
-        />
-        <span
-          v-if="hasVideoAttachments"
-          class="icon-video"
-        />
-        <span
-          v-if="status.card"
-          class="icon-link"
-        />
-      </a>
       <div
-        class="status-content media-body"
+        class="media-body summary"
         @click.prevent="linkClicked"
-        v-html="postBodyHtml"
+        v-html="status.summary_html"
       />
       <a
-        v-if="showingLongSubject"
+        v-if="longSubject && showingLongSubject"
         href="#"
-        class="status-unhider"
+        class="tall-subject-hider"
         @click.prevent="showingLongSubject=false"
       >{{ $t("general.show_less") }}</a>
+      <a
+        v-else-if="longSubject"
+        class="tall-subject-hider"
+        :class="{ 'tall-subject-hider_focused': focused }"
+        href="#"
+        @click.prevent="showingLongSubject=true"
+      >
+        {{ $t("general.show_more") }}
+      </a>
     </div>
     <div
-      v-else
       :class="{'tall-status': hideTallStatus}"
       class="status-content-wrapper"
     >
@@ -51,13 +38,9 @@
         :class="{ 'tall-status-hider_focused': focused }"
         href="#"
         @click.prevent="toggleShowMore"
-      >{{ $t("general.show_more") }}</a>
-      <div
-        v-if="status.summary_html"
-        class="status-content media-body summary"
-        @click.prevent="linkClicked"
-        v-html="status.summary_html"
-      />
+      >
+        {{ $t("general.show_more") }}
+      </a>
       <div
         v-if="!hideSubjectStatus"
         class="status-content media-body"
@@ -69,7 +52,21 @@
         href="#"
         class="cw-status-hider"
         @click.prevent="toggleShowMore"
-      >{{ $t("general.show_more") }}</a>
+      >
+        {{ $t("general.show_more") }}
+        <span
+          v-if="hasImageAttachments"
+          class="icon-picture"
+        />
+        <span
+          v-if="hasVideoAttachments"
+          class="icon-video"
+        />
+        <span
+          v-if="status.card"
+          class="icon-link"
+        />
+      </a>
       <a
         v-if="showingMore"
         href="#"
@@ -129,6 +126,12 @@ $status-margin: 0.75em;
   flex: 1;
   min-width: 0;
 
+  .status-content-wrapper {
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+  }
+
   .tall-status {
     position: relative;
     height: 220px;
@@ -136,7 +139,7 @@ $status-margin: 0.75em;
     overflow-y: hidden;
     z-index: 1;
     .status-content {
-      height: 100%;
+      min-height: 0;
       mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
             linear-gradient(to top, white, white);
       /* Autoprefixed seem to ignore this one, and also syntax is different */
@@ -176,15 +179,43 @@ $status-margin: 0.75em;
     }
   }
 
+  .summary-wrapper {
+    margin-bottom: 0.5em;
+    border-style: solid;
+    border-width: 0 0 1px 0;
+    border-color: var(--border, $fallback--border);
+    flex-grow: 0;
+  }
+
+  .summary {
+    font-style: italic;
+    padding-bottom: 0.5em;
+  }
+
+  .tall-subject {
+    position: relative;
+    .summary {
+      max-height: 2em;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+  }
+
+  .tall-subject-hider {
+    display: inline-block;
+    word-break: break-all;
+    // position: absolute;
+    width: 100%;
+    text-align: center;
+    padding-bottom: 0.5em;
+  }
+
   .status-content {
     font-family: var(--postFont, sans-serif);
     line-height: 1.4em;
     white-space: pre-wrap;
 
-    &.summary {
-      font-weight: bold;
-    }
-
     blockquote {
       margin: 0.2em 0 0.2em 2em;
       font-style: italic;
-- 
cgit v1.2.3-70-g09d2


From 8c3106c588f964a8e26c69784d5808ad1f72625c Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Fri, 26 Jun 2020 18:20:32 +0300
Subject: Change the show/hide strings about, remove subjected status toggle
 when 'collapse' option not used

---
 src/components/status_content/status_content.js  |  4 ++--
 src/components/status_content/status_content.vue | 10 ++++++----
 src/i18n/en.json                                 |  6 +++++-
 3 files changed, 13 insertions(+), 7 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 66501b3e..09ea3a20 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -48,10 +48,10 @@ const StatusContent = {
     },
     // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
     mightHideBecauseSubject () {
-      return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
+      return !!this.status.summary && this.localCollapseSubjectDefault
     },
     mightHideBecauseTall () {
-      return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+      return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
     },
     hideSubjectStatus () {
       return this.mightHideBecauseSubject && !this.expandingSubject
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 5698b4c0..3460c2fa 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -17,7 +17,7 @@
         href="#"
         class="tall-subject-hider"
         @click.prevent="showingLongSubject=false"
-      >{{ $t("general.show_less") }}</a>
+      >{{ $t("status.hide_full_subject") }}</a>
       <a
         v-else-if="longSubject"
         class="tall-subject-hider"
@@ -25,7 +25,7 @@
         href="#"
         @click.prevent="showingLongSubject=true"
       >
-        {{ $t("general.show_more") }}
+        {{ $t("status.show_full_subject") }}
       </a>
     </div>
     <div
@@ -53,7 +53,7 @@
         class="cw-status-hider"
         @click.prevent="toggleShowMore"
       >
-        {{ $t("general.show_more") }}
+        {{ $t("status.show_content") }}
         <span
           v-if="hasImageAttachments"
           class="icon-picture"
@@ -72,7 +72,9 @@
         href="#"
         class="status-unhider"
         @click.prevent="toggleShowMore"
-      >{{ $t("general.show_less") }}</a>
+      >
+        {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
+      </a>
     </div>
 
     <div v-if="status.poll && status.poll.options">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eefe10e5..ede8c3d8 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -629,7 +629,11 @@
     "status_unavailable": "Status unavailable",
     "copy_link": "Copy link to status",
     "thread_muted": "Thread muted",
-    "thread_muted_and_words": ", has words:"
+    "thread_muted_and_words": ", has words:",
+    "show_full_subject": "Show full subject",
+    "hide_full_subject": "Hide full subject",
+    "show_content": "Show content",
+    "hide_content": "Hide content"
   },
   "user_card": {
     "approve": "Approve",
-- 
cgit v1.2.3-70-g09d2


From 61f34ff361a24a287ac0bf0c79fd2f77b4f2b708 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Sun, 28 Jun 2020 16:40:39 +0300
Subject: remove panel-footer in userpanel, simplify preview header, fix
 word-wrap in preview

---
 .../post_status_form/post_status_form.js           | 17 ++++--
 .../post_status_form/post_status_form.vue          | 68 +++++++++++-----------
 src/components/status/status.vue                   |  3 -
 src/components/status_content/status_content.vue   |  3 +
 src/components/user_panel/user_panel.vue           |  4 +-
 src/i18n/en.json                                   |  1 -
 6 files changed, 48 insertions(+), 48 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index bb5dbf97..732691e7 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -235,13 +235,13 @@ const PostStatusForm = {
         this.posting = false
       })
     },
-    previewStatus (newStatus) {
+    previewStatus () {
       if (this.emptyStatus) {
         this.preview = { error: this.$t('status.preview_empty') }
         this.previewLoading = false
         return
       }
-
+      const newStatus = this.newStatus
       this.previewLoading = true
       statusPoster.postStatus({
         status: newStatus.status,
@@ -269,18 +269,23 @@ const PostStatusForm = {
         this.previewLoading = false
       })
     },
-    debouncePreviewStatus: debounce(function (newStatus) {
-      this.previewStatus(newStatus)
-    }, 750),
+    debouncePreviewStatus: debounce(function () { this.previewStatus() }, 750),
     autoPreview () {
       if (!this.preview) return
       this.previewLoading = true
-      this.debouncePreviewStatus(this.newStatus)
+      this.debouncePreviewStatus()
     },
     closePreview () {
       this.preview = null
       this.previewLoading = false
     },
+    togglePreview () {
+      if (this.showPreview) {
+        this.closePreview()
+      } else {
+        this.previewStatus()
+      }
+    },
     addMediaFile (fileInfo) {
       this.newStatus.files.push(fileInfo)
     },
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 8201911e..0cebd36e 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -16,30 +16,26 @@
         @drop.stop="fileDrop"
       />
       <div class="form-group">
-        <a
-          v-if="!showPreview"
-          class="preview-start"
-          @click.stop.prevent="previewStatus(newStatus)"
-        >
-          {{ $t('status.preview') }}
-        </a>
+        <div class="preview-heading faint">
+          <a
+            class="preview-toggle faint"
+            @click.stop.prevent="togglePreview"
+          >
+            {{ $t('status.preview') }}
+            <i
+              class="icon-down-open"
+              :style="{ transform: showPreview ? 'rotate(0deg)' : 'rotate(-90deg)' }"
+            />
+          </a>
+          <i
+            v-show="previewLoading"
+            class="icon-spin3 animate-spin"
+          />
+        </div>
         <div
-          v-else
+          v-if="showPreview"
           class="preview-container"
         >
-          <span class="preview-heading">
-            <span class="preview-title">
-              {{ $t('status.status_preview') }}
-            </span>
-            <i
-              v-if="previewLoading"
-              class="icon-spin3 animate-spin"
-            />
-            <i
-              class="preview-close icon-cancel"
-              @click.stop.prevent="closePreview"
-            />
-          </span>
           <div
             v-if="!preview"
             class="preview-status"
@@ -369,28 +365,30 @@
     max-width: 10em;
   }
 
-  .preview-start {
-    margin-left: auto;
-    cursor: pointer;
-  }
+  .preview-heading {
+    display: flex;
+    width: 100%;
 
-  .preview-container {
-    margin-bottom: 1em;
+    .icon-spin3 {
+      margin-left: auto;
+    }
   }
 
-  .preview-heading {
+  .preview-toggle {
     display: flex;
-    width: 100%;
-    color: $fallback--faint;
-    color: var(--faint, $fallback--faint);
+    cursor: pointer;
+
+    &:hover {
+      text-decoration: underline;
+    }
   }
 
-  .preview-title {
-    flex-grow: 1;
+  .icon-down-open {
+    transition: transform 0.1s;
   }
 
-  .preview-close {
-    padding-left: 0.5em;
+  .preview-container {
+    margin-bottom: 1em;
   }
 
   .preview-error {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 7ec29b28..1c36d883 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -372,9 +372,6 @@ $status-margin: 0.75em;
 }
 
 .status-el {
-  overflow-wrap: break-word;
-  word-wrap: break-word;
-  word-break: break-word;
   border-left-width: 0px;
   min-width: 0;
   border-color: $fallback--border;
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index efc2485e..373802af 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -180,6 +180,9 @@ $status-margin: 0.75em;
     font-family: var(--postFont, sans-serif);
     line-height: 1.4em;
     white-space: pre-wrap;
+    overflow-wrap: break-word;
+    word-wrap: break-word;
+    word-break: break-word;
 
     blockquote {
       margin: 0.2em 0 0.2em 2em;
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 1db4f648..5685916a 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -10,9 +10,7 @@
         :hide-bio="true"
         rounded="top"
       />
-      <div class="panel-footer">
-        <PostStatusForm />
-      </div>
+      <PostStatusForm />
     </div>
     <auth-form
       v-else
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 44afb59c..21df3e73 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -638,7 +638,6 @@
     "thread_muted": "Thread muted",
     "thread_muted_and_words": ", has words:",
     "preview": "Preview",
-    "status_preview": "Status preview",
     "preview_empty": "Empty"
   },
   "user_card": {
-- 
cgit v1.2.3-70-g09d2


From aa2cf51c05ebdf411d74af5debbbc8fa4d3cf457 Mon Sep 17 00:00:00 2001
From: eugenijm <eugenijm@protonmail.com>
Date: Thu, 7 May 2020 16:10:53 +0300
Subject: Add Chats

---
 src/App.js                                         |  16 +-
 src/App.scss                                       |  14 +
 src/App.vue                                        |   3 +-
 src/_variables.scss                                |   1 +
 src/boot/after_store.js                            |   1 +
 src/boot/routes.js                                 |  15 +-
 src/components/account_actions/account_actions.js  |  12 +
 src/components/account_actions/account_actions.vue |   7 +
 src/components/chat/chat.js                        | 304 +++++++++++++++++++++
 src/components/chat/chat.scss                      | 161 +++++++++++
 src/components/chat/chat.vue                       |  99 +++++++
 src/components/chat/chat_layout.js                 | 100 +++++++
 src/components/chat/chat_layout_utils.js           |  27 ++
 src/components/chat_avatar/chat_avatar.js          |  23 ++
 src/components/chat_avatar/chat_avatar.vue         |  53 ++++
 src/components/chat_list/chat_list.js              |  37 +++
 src/components/chat_list/chat_list.vue             |  48 ++++
 src/components/chat_list_item/chat_list_item.js    |  65 +++++
 src/components/chat_list_item/chat_list_item.scss  |  94 +++++++
 src/components/chat_list_item/chat_list_item.vue   |  49 ++++
 src/components/chat_message/chat_message.js        | 109 ++++++++
 src/components/chat_message/chat_message.scss      | 157 +++++++++++
 src/components/chat_message/chat_message.vue       |  99 +++++++
 .../chat_message_date/chat_message_date.vue        |  24 ++
 src/components/chat_new/chat_new.js                |  74 +++++
 src/components/chat_new/chat_new.scss              |  29 ++
 src/components/chat_new/chat_new.vue               |  46 ++++
 src/components/chat_panel/chat_panel.vue           |  80 +++---
 src/components/chat_title/chat_title.js            |  20 ++
 src/components/chat_title/chat_title.vue           |  56 ++++
 src/components/emoji_input/emoji_input.js          |  38 ++-
 src/components/emoji_input/emoji_input.vue         |   5 +-
 src/components/features_panel/features_panel.js    |   1 +
 src/components/features_panel/features_panel.vue   |   3 +
 src/components/media_upload/media_upload.js        |   3 +-
 src/components/media_upload/media_upload.vue       |  21 +-
 src/components/mobile_nav/mobile_nav.js            |   5 +-
 src/components/mobile_nav/mobile_nav.vue           |   1 +
 .../mobile_post_status_button.js                   |   7 +
 src/components/nav_panel/nav_panel.js              |  20 +-
 src/components/nav_panel/nav_panel.vue             |  11 +
 src/components/notification/notification.js        |   6 +-
 src/components/notifications/notifications.js      |   9 +-
 .../post_status_form/post_status_form.js           | 138 +++++++---
 .../post_status_form/post_status_form.vue          |  50 +++-
 .../settings_modal/tabs/theme_tab/theme_tab.js     |   6 +-
 .../settings_modal/tabs/theme_tab/theme_tab.vue    |  67 +++++
 src/components/side_drawer/side_drawer.js          |   7 +-
 src/components/side_drawer/side_drawer.vue         |  22 +-
 src/components/status_content/status_content.js    |   2 +-
 src/components/status_content/status_content.vue   |   2 +-
 src/hocs/with_load_more/with_load_more.scss        |   4 +
 src/i18n/en.json                                   |  33 ++-
 src/main.js                                        |   4 +-
 src/modules/api.js                                 |  24 +-
 src/modules/chats.js                               | 228 ++++++++++++++++
 src/modules/config.js                              |   3 +-
 src/modules/instance.js                            |   1 +
 src/modules/interface.js                           |   9 +-
 src/modules/statuses.js                            |   5 +-
 src/modules/users.js                               |   5 +
 src/services/api/api.service.js                    | 117 +++++++-
 src/services/chat_service/chat_service.js          | 150 ++++++++++
 .../entity_normalizer/entity_normalizer.service.js |  34 ++-
 src/services/style_setter/style_setter.js          |   3 +-
 src/services/theme_data/pleromafe.js               |  53 +++-
 src/services/window_utils/window_utils.js          |   5 +
 static/fontello.json                               |   6 +
 test/unit/specs/boot/routes.spec.js                |  10 +-
 69 files changed, 2787 insertions(+), 154 deletions(-)
 create mode 100644 src/components/chat/chat.js
 create mode 100644 src/components/chat/chat.scss
 create mode 100644 src/components/chat/chat.vue
 create mode 100644 src/components/chat/chat_layout.js
 create mode 100644 src/components/chat/chat_layout_utils.js
 create mode 100644 src/components/chat_avatar/chat_avatar.js
 create mode 100644 src/components/chat_avatar/chat_avatar.vue
 create mode 100644 src/components/chat_list/chat_list.js
 create mode 100644 src/components/chat_list/chat_list.vue
 create mode 100644 src/components/chat_list_item/chat_list_item.js
 create mode 100644 src/components/chat_list_item/chat_list_item.scss
 create mode 100644 src/components/chat_list_item/chat_list_item.vue
 create mode 100644 src/components/chat_message/chat_message.js
 create mode 100644 src/components/chat_message/chat_message.scss
 create mode 100644 src/components/chat_message/chat_message.vue
 create mode 100644 src/components/chat_message_date/chat_message_date.vue
 create mode 100644 src/components/chat_new/chat_new.js
 create mode 100644 src/components/chat_new/chat_new.scss
 create mode 100644 src/components/chat_new/chat_new.vue
 create mode 100644 src/components/chat_title/chat_title.js
 create mode 100644 src/components/chat_title/chat_title.vue
 create mode 100644 src/modules/chats.js
 create mode 100644 src/services/chat_service/chat_service.js

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/App.js b/src/App.js
index 92c4e2f5..84300e00 100644
--- a/src/App.js
+++ b/src/App.js
@@ -14,7 +14,7 @@ import MobileNav from './components/mobile_nav/mobile_nav.vue'
 import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
 import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
 import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
-import { windowWidth } from './services/window_utils/window_utils'
+import { windowWidth, windowHeight } from './services/window_utils/window_utils'
 
 export default {
   name: 'app',
@@ -45,7 +45,8 @@ export default {
         window.CSS.supports('-moz-mask-size', 'contain') ||
         window.CSS.supports('-ms-mask-size', 'contain') ||
         window.CSS.supports('-o-mask-size', 'contain')
-    )
+    ),
+    transitionName: 'fade'
   }),
   created () {
     // Load the locale from the storage
@@ -127,10 +128,21 @@ export default {
     },
     updateMobileState () {
       const mobileLayout = windowWidth() <= 800
+      const layoutHeight = windowHeight()
       const changed = mobileLayout !== this.isMobileLayout
       if (changed) {
         this.$store.dispatch('setMobileLayout', mobileLayout)
       }
+      this.$store.dispatch('setLayoutHeight', layoutHeight)
+    }
+  },
+  watch: {
+    '$route' (to, from) {
+      if ((to.name === 'chat' && from.name === 'chats') || (to.name === 'chats' && from.name === 'chat')) {
+        this.transitionName = 'none'
+      } else {
+        this.transitionName = 'fade'
+      }
     }
   }
 }
diff --git a/src/App.scss b/src/App.scss
index 6597b6f4..29ce73a8 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -56,6 +56,7 @@ body {
   overflow-x: hidden;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
+  overscroll-behavior: none;
 
   &.hidden {
     display: none;
@@ -928,3 +929,16 @@ nav {
   background-color: $fallback--fg;
   background-color: var(--panel, $fallback--fg);
 }
+
+.unread-chat-count {
+  font-size: 0.9em;
+  font-weight: bolder;
+  font-style: normal;
+  position: absolute;
+  right: 0.6rem;
+  padding: 0 0.3em;
+  min-width: 1.3rem;
+  min-height: 1.3rem;
+  max-height: 1.3rem;
+  line-height: 1.3rem;
+}
diff --git a/src/App.vue b/src/App.vue
index 03b632ec..5d429934 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -77,6 +77,7 @@
         </div>
       </div>
     </nav>
+    <div class="app-bg-wrapper app-container-wrapper" />
     <div
       id="content"
       class="container underlay"
@@ -112,7 +113,7 @@
             {{ $t("login.hint") }}
           </router-link>
         </div>
-        <transition name="fade">
+        <transition :name="transitionName">
           <router-view />
         </transition>
       </div>
diff --git a/src/_variables.scss b/src/_variables.scss
index 30dc3e42..9004d551 100644
--- a/src/_variables.scss
+++ b/src/_variables.scss
@@ -27,5 +27,6 @@ $fallback--tooltipRadius: 5px;
 $fallback--avatarRadius: 4px;
 $fallback--avatarAltRadius: 10px;
 $fallback--attachmentRadius: 10px;
+$fallback--chatMessageRadius: 10px;
 
 $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 302b278c..7160434f 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -230,6 +230,7 @@ const getNodeInfo = async ({ store }) => {
       store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
       store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
       store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
+      store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
       store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
       store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
       store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
diff --git a/src/boot/routes.js b/src/boot/routes.js
index f63d8adf..b5d3c631 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -6,6 +6,8 @@ import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue
 import ConversationPage from 'components/conversation-page/conversation-page.vue'
 import Interactions from 'components/interactions/interactions.vue'
 import DMs from 'components/dm_timeline/dm_timeline.vue'
+import ChatList from 'components/chat_list/chat_list.vue'
+import Chat from 'components/chat/chat.vue'
 import UserProfile from 'components/user_profile/user_profile.vue'
 import Search from 'components/search/search.vue'
 import Registration from 'components/registration/registration.vue'
@@ -28,7 +30,7 @@ export default (store) => {
     }
   }
 
-  return [
+  let routes = [
     { name: 'root',
       path: '/',
       redirect: _to => {
@@ -62,11 +64,20 @@ export default (store) => {
     { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
     { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
     { name: 'login', path: '/login', component: AuthForm },
-    { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
+    { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) },
     { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
     { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
     { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
     { name: 'about', path: '/about', component: About },
     { name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
   ]
+
+  if (store.state.instance.pleromaChatMessagesAvailable) {
+    routes = routes.concat([
+      { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
+      { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
+    ])
+  }
+
+  return routes
 }
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 0826c275..6d345bc7 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -1,3 +1,4 @@
+import { mapState } from 'vuex'
 import ProgressButton from '../progress_button/progress_button.vue'
 import Popover from '../popover/popover.vue'
 
@@ -27,7 +28,18 @@ const AccountActions = {
     },
     reportUser () {
       this.$store.dispatch('openUserReportingModal', this.user.id)
+    },
+    openChat () {
+      this.$router.push({
+        name: 'chat',
+        params: { recipient_id: this.user.id }
+      })
     }
+  },
+  computed: {
+    ...mapState({
+      pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
+    })
   }
 }
 
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 029e7096..987e94b7 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -50,6 +50,13 @@
           >
             {{ $t('user_card.report') }}
           </button>
+          <button
+            v-if="pleromaChatMessagesAvailable"
+            class="btn btn-default btn-block dropdown-item"
+            @click="openChat"
+          >
+            {{ $t('user_card.message') }}
+          </button>
         </div>
       </div>
       <div
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
new file mode 100644
index 00000000..6e23c20c
--- /dev/null
+++ b/src/components/chat/chat.js
@@ -0,0 +1,304 @@
+import _ from 'lodash'
+import { WSConnectionStatus } from '../../services/api/api.service.js'
+import { mapGetters, mapState } from 'vuex'
+import ChatMessage from '../chat_message/chat_message.vue'
+import ChatAvatar from '../chat_avatar/chat_avatar.vue'
+import PostStatusForm from '../post_status_form/post_status_form.vue'
+import ChatTitle from '../chat_title/chat_title.vue'
+import chatService from '../../services/chat_service/chat_service.js'
+import ChatLayout from './chat_layout.js'
+import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
+
+const BOTTOMED_OUT_OFFSET = 10
+const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
+
+const Chat = {
+  components: {
+    ChatMessage,
+    ChatTitle,
+    ChatAvatar,
+    PostStatusForm
+  },
+  mixins: [ChatLayout],
+  data () {
+    return {
+      jumpToBottomButtonVisible: false,
+      hoveredMessageChainId: undefined,
+      scrollPositionBeforeResize: {},
+      scrollableContainerHeight: '100%',
+      errorLoadingChat: false
+    }
+  },
+  created () {
+    this.startFetching()
+    window.addEventListener('resize', this.handleLayoutChange)
+  },
+  mounted () {
+    window.addEventListener('scroll', this.handleScroll)
+    if (typeof document.hidden !== 'undefined') {
+      document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
+    }
+
+    this.$nextTick(() => {
+      this.updateScrollableContainerHeight()
+      this.handleResize()
+    })
+    this.setChatLayout()
+  },
+  destroyed () {
+    window.removeEventListener('scroll', this.handleScroll)
+    window.removeEventListener('resize', this.handleLayoutChange)
+    this.unsetChatLayout()
+    if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
+    this.$store.dispatch('clearCurrentChat')
+  },
+  computed: {
+    recipient () {
+      return this.currentChat && this.currentChat.account
+    },
+    recipientId () {
+      return this.$route.params.recipient_id
+    },
+    formPlaceholder () {
+      if (this.recipient) {
+        return this.$t('chats.message_user', { nickname: this.recipient.screen_name })
+      } else {
+        return ''
+      }
+    },
+    chatViewItems () {
+      return chatService.getView(this.currentChatMessageService)
+    },
+    newMessageCount () {
+      return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
+    },
+    streamingEnabled () {
+      return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
+    },
+    ...mapGetters([
+      'currentChat',
+      'currentChatMessageService',
+      'findOpenedChatByRecipientId',
+      'mergedConfig'
+    ]),
+    ...mapState({
+      backendInteractor: state => state.api.backendInteractor,
+      mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
+      mobileLayout: state => state.interface.mobileLayout,
+      layoutHeight: state => state.interface.layoutHeight,
+      currentUser: state => state.users.currentUser
+    })
+  },
+  watch: {
+    chatViewItems () {
+      // We don't want to scroll to the bottom on a new message when the user is viewing older messages.
+      // Therefore we need to know whether the scroll position was at the bottom before the DOM update.
+      const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
+      this.$nextTick(() => {
+        if (bottomedOutBeforeUpdate) {
+          this.scrollDown({ forceRead: !document.hidden })
+        }
+      })
+    },
+    '$route': function () {
+      this.startFetching()
+    },
+    layoutHeight () {
+      this.handleResize({ expand: true })
+    },
+    mastoUserSocketStatus (newValue) {
+      if (newValue === WSConnectionStatus.JOINED) {
+        this.fetchChat({ isFirstFetch: true })
+      }
+    }
+  },
+  methods: {
+    // Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
+    onMessageHover ({ isHovered, messageChainId }) {
+      this.hoveredMessageChainId = isHovered ? messageChainId : undefined
+    },
+    onFilesDropped () {
+      this.$nextTick(() => {
+        this.updateScrollableContainerHeight()
+      })
+    },
+    handleVisibilityChange () {
+      this.$nextTick(() => {
+        if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
+          this.scrollDown({ forceRead: true })
+        }
+      })
+    },
+    handleLayoutChange () {
+      this.updateScrollableContainerHeight()
+      if (this.mobileLayout) {
+        this.setMobileChatLayout()
+      } else {
+        this.unsetMobileChatLayout()
+      }
+      this.$nextTick(() => {
+        this.updateScrollableContainerHeight()
+        this.scrollDown()
+      })
+    },
+    // Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
+    updateScrollableContainerHeight () {
+      const header = this.$refs.header
+      const footer = this.$refs.footer
+      const inner = this.mobileLayout ? window.document.body : this.$refs.inner
+      this.scrollableContainerHeight = scrollableContainerHeight(inner, header, footer) + 'px'
+    },
+    // Preserves the scroll position when OSK appears or the posting form changes its height.
+    handleResize (opts) {
+      this.$nextTick(() => {
+        this.updateScrollableContainerHeight()
+
+        const { offsetHeight = undefined } = this.scrollPositionBeforeResize
+        this.scrollPositionBeforeResize = getScrollPosition(this.$refs.scrollable)
+
+        const diff = this.scrollPositionBeforeResize.offsetHeight - offsetHeight
+        if (diff < 0 || (!this.bottomedOut() && opts && opts.expand)) {
+          this.$nextTick(() => {
+            this.updateScrollableContainerHeight()
+            this.$refs.scrollable.scrollTo({
+              top: this.$refs.scrollable.scrollTop - diff,
+              left: 0
+            })
+          })
+        }
+      })
+    },
+    scrollDown (options = {}) {
+      const { behavior = 'auto', forceRead = false } = options
+      const scrollable = this.$refs.scrollable
+      if (!scrollable) { return }
+      this.$nextTick(() => {
+        scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
+      })
+      if (forceRead || this.newMessageCount > 0) {
+        this.readChat()
+      }
+    },
+    readChat () {
+      if (!(this.currentChatMessageService && this.currentChatMessageService.lastMessage)) { return }
+      if (document.hidden) { return }
+      const lastReadId = this.currentChatMessageService.lastMessage.id
+      this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId })
+    },
+    bottomedOut (offset) {
+      return isBottomedOut(this.$refs.scrollable, offset)
+    },
+    reachedTop () {
+      const scrollable = this.$refs.scrollable
+      return scrollable && scrollable.scrollTop <= 0
+    },
+    handleScroll: _.throttle(function () {
+      if (!this.currentChat) { return }
+
+      if (this.reachedTop()) {
+        this.fetchChat({ maxId: this.currentChatMessageService.minId })
+      } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
+        this.jumpToBottomButtonVisible = false
+        if (this.newMessageCount > 0) {
+          this.readChat()
+        }
+      } else {
+        this.jumpToBottomButtonVisible = true
+      }
+    }, 100),
+    handleScrollUp (positionBeforeLoading) {
+      const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
+      this.$refs.scrollable.scrollTo({
+        top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
+        left: 0
+      })
+    },
+    fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
+      const chatMessageService = this.currentChatMessageService
+      if (!chatMessageService) { return }
+      if (fetchLatest && this.streamingEnabled) { return }
+
+      const chatId = chatMessageService.chatId
+      const fetchOlderMessages = !!maxId
+      const sinceId = fetchLatest && chatMessageService.lastMessage && chatMessageService.lastMessage.id
+
+      this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
+        .then((messages) => {
+          // Clear the current chat in case we're recovering from a ws connection loss.
+          if (isFirstFetch) {
+            chatService.clear(chatMessageService)
+          }
+
+          const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
+          this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
+            this.$nextTick(() => {
+              if (fetchOlderMessages) {
+                this.handleScrollUp(positionBeforeUpdate)
+              }
+
+              if (isFirstFetch) {
+                this.updateScrollableContainerHeight()
+              }
+            })
+          })
+        })
+    },
+    async startFetching () {
+      let chat = this.findOpenedChatByRecipientId(this.recipientId)
+      if (!chat) {
+        try {
+          chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
+        } catch (e) {
+          console.error('Error creating or getting a chat', e)
+          this.errorLoadingChat = true
+        }
+      }
+      if (chat) {
+        this.$nextTick(() => {
+          this.scrollDown({ forceRead: true })
+        })
+        this.$store.dispatch('addOpenedChat', { chat })
+        this.doStartFetching()
+      }
+    },
+    doStartFetching () {
+      this.$store.dispatch('startFetchingCurrentChat', {
+        fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
+      })
+      this.fetchChat({ isFirstFetch: true })
+    },
+    sendMessage ({ status, media }) {
+      const params = {
+        id: this.currentChat.id,
+        content: status
+      }
+
+      if (media[0]) {
+        params.mediaId = media[0].id
+      }
+
+      return this.backendInteractor.sendChatMessage(params)
+        .then(data => {
+          this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => {
+            this.$nextTick(() => {
+              this.updateScrollableContainerHeight()
+              this.scrollDown({ forceRead: true })
+            })
+          })
+
+          return data
+        })
+        .catch(error => {
+          console.error('Error sending message', error)
+          return {
+            error: this.$t('chats.error_sending_message')
+          }
+        })
+    },
+    goBack () {
+      this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
+    }
+  }
+}
+
+export default Chat
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
new file mode 100644
index 00000000..13c52ea3
--- /dev/null
+++ b/src/components/chat/chat.scss
@@ -0,0 +1,161 @@
+.chat-view {
+  display: flex;
+  height: calc(100vh - 60px);
+  width: 100%;
+
+  .chat-view-inner {
+    height: auto;
+    width: 100%;
+    overflow: visible;
+    display: flex;
+    margin-top: 0.5em;
+    margin-left: 0.5em;
+    margin-right: 0.5em;
+  }
+
+  .chat-view-body {
+    background-color: var(--chatBg, $fallback--bg);
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    overflow: visible;
+    border-radius: none;
+    min-height: 100%;
+    margin-left: 0;
+    margin-right: 0;
+    margin-bottom: 0em;
+    margin-top: 0em;
+    border-radius: 10px 10px 0 0;
+    border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ;
+
+    &::after {
+      border-radius: none;
+      box-shadow: none;
+    }
+  }
+
+  .scrollable-message-list {
+    padding: 0 10px;
+    height: 100%;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .footer {
+    position: sticky;
+    bottom: 0px;
+  }
+
+  .chat-view-heading {
+    align-items: center;
+    justify-content: space-between;
+    top: 50px;
+    display: flex;
+    z-index: 2;
+    border-radius: none;
+    position: sticky;
+    display: flex;
+    overflow: hidden;
+  }
+
+  .go-back-button {
+    margin-right: 1.2em;
+    cursor: pointer;
+  }
+
+  .jump-to-bottom-button {
+    width: 2.5em;
+    height: 2.5em;
+    border-radius: 100%;
+    position: absolute;
+    right: 1.3em;
+    top: -3.2em;
+    background-color: $fallback--fg;
+    background-color: var(--btn, $fallback--fg);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3);
+    z-index: 10;
+    transition: 0.35s all;
+    transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
+    opacity: 0;
+    visibility: hidden;
+    cursor: pointer;
+
+    &.visible {
+      opacity: 1;
+      visibility: visible;
+    }
+
+    i {
+      font-size: 1em;
+      color: $fallback--text;
+      color: var(--text, $fallback--text);
+    }
+
+    .unread-message-count {
+      font-size: 0.8em;
+      left: 50%;
+      transform: translate(-50%, 0);
+      border-radius: 100%;
+      margin-top: -1rem;
+      padding: 0;
+    }
+
+    .chat-loading-error {
+      width: 100%;
+      display: flex;
+      align-items: flex-end;
+      height: 100%;
+
+      .error {
+        width: 100%;
+      }
+    }
+  }
+
+  @media all and (max-width: 800px) {
+    height: 100%;
+    overflow: hidden;
+
+    .chat-view-inner {
+      overflow: hidden;
+      height: 100%;
+      margin-top: 0;
+      margin-left: 0;
+      margin-right: 0;
+    }
+
+    .chat-view-body {
+      display: flex;
+      min-height: auto;
+      overflow: hidden;
+      height: 100%;
+      margin: 0;
+      border-radius: 0 !important;
+    }
+
+    .chat-view-heading {
+      position: static;
+      z-index: 9999;
+      top: 0;
+      margin-top: 0;
+      border-radius: 0;
+    }
+
+    .scrollable-message-list {
+      display: unset;
+      overflow-y: scroll;
+      overflow-x: hidden;
+      -webkit-overflow-scrolling: touch;
+    }
+
+    .footer {
+      position: sticky;
+      bottom: auto;
+    }
+  }
+}
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
new file mode 100644
index 00000000..d8c91dbe
--- /dev/null
+++ b/src/components/chat/chat.vue
@@ -0,0 +1,99 @@
+<template>
+  <div class="chat-view">
+    <div class="chat-view-inner">
+      <div
+        id="nav"
+        ref="inner"
+        class="panel-default panel chat-view-body"
+      >
+        <div
+          ref="header"
+          class="panel-heading chat-view-heading mobile-hidden"
+        >
+          <a
+            class="go-back-button"
+            @click="goBack"
+          >
+            <i class="button-icon icon-left-open" />
+          </a>
+          <div class="title text-center">
+            <ChatTitle
+              :user="recipient"
+              :with-avatar="true"
+            />
+          </div>
+        </div>
+        <template>
+          <div
+            ref="scrollable"
+            class="scrollable-message-list"
+            :style="{ height: scrollableContainerHeight }"
+            @scroll="handleScroll"
+          >
+            <template v-if="!errorLoadingChat">
+              <ChatMessage
+                v-for="chatViewItem in chatViewItems"
+                :key="chatViewItem.id"
+                :author="recipient"
+                :chat-view-item="chatViewItem"
+                :hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
+                @hover="onMessageHover"
+              />
+            </template>
+            <div
+              v-else
+              class="chat-loading-error"
+            >
+              <div class="alert error">
+                {{ $t('chats.error_loading_chat') }}
+              </div>
+            </div>
+          </div>
+          <div
+            ref="footer"
+            class="panel-body footer"
+          >
+            <div
+              class="jump-to-bottom-button"
+              :class="{ 'visible': jumpToBottomButtonVisible }"
+              @click="scrollDown({ behavior: 'smooth' })"
+            >
+              <i class="icon-down-open">
+                <div
+                  v-if="newMessageCount"
+                  class="badge badge-notification unread-chat-count unread-message-count"
+                >
+                  {{ newMessageCount }}
+                </div>
+              </i>
+            </div>
+            <PostStatusForm
+              :disable-subject="true"
+              :disable-scope-selector="true"
+              :disable-notice="true"
+              :disable-lock-warning="true"
+              :disable-polls="true"
+              :disable-sensitivity-checkbox="true"
+              :disable-submit="errorLoadingChat || !currentChat"
+              :request="sendMessage"
+              :submit-on-enter="!mobileLayout"
+              :preserve-focus="!mobileLayout"
+              :auto-focus="!mobileLayout"
+              :placeholder="formPlaceholder"
+              :file-limit="1"
+              max-height="160"
+              emoji-picker-placement="top"
+              @resize="handleResize"
+            />
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./chat.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+@import './chat.scss';
+</style>
diff --git a/src/components/chat/chat_layout.js b/src/components/chat/chat_layout.js
new file mode 100644
index 00000000..07ae3abf
--- /dev/null
+++ b/src/components/chat/chat_layout.js
@@ -0,0 +1,100 @@
+const ChatLayout = {
+  methods: {
+    setChatLayout () {
+      if (this.mobileLayout) {
+        this.setMobileChatLayout()
+      }
+    },
+    unsetChatLayout () {
+      this.unsetMobileChatLayout()
+    },
+    setMobileChatLayout () {
+      // This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
+      // This layout prevents empty spaces from being visible at the bottom
+      // of the chat on iOS Safari (`safe-area-inset`) when
+      // - the on-screen keyboard appears and the user starts typing
+      // - the user selects the text inside the input area
+      // - the user selects and deletes the text that is multiple lines long
+      // TODO: unify the chat layout with the global layout.
+
+      let html = document.querySelector('html')
+      if (html) {
+        html.style.overflow = 'hidden'
+        html.style.height = '100%'
+      }
+
+      let body = document.querySelector('body')
+      if (body) {
+        body.style.height = '100%'
+      }
+
+      let app = document.getElementById('app')
+      if (app) {
+        app.style.height = '100%'
+        app.style.overflow = 'hidden'
+        app.style.minHeight = 'auto'
+      }
+
+      let appBgWrapper = window.document.getElementById('app_bg_wrapper')
+      if (appBgWrapper) {
+        appBgWrapper.style.overflow = 'hidden'
+      }
+
+      let main = document.getElementsByClassName('main')[0]
+      if (main) {
+        main.style.overflow = 'hidden'
+        main.style.height = '100%'
+      }
+
+      let content = document.getElementById('content')
+      if (content) {
+        content.style.paddingTop = '0'
+        content.style.height = '100%'
+        content.style.overflow = 'visible'
+      }
+
+      this.$nextTick(() => {
+        this.updateScrollableContainerHeight()
+      })
+    },
+    unsetMobileChatLayout () {
+      let html = document.querySelector('html')
+      if (html) {
+        html.style.overflow = 'visible'
+        html.style.height = 'unset'
+      }
+
+      let body = document.querySelector('body')
+      if (body) {
+        body.style.height = 'unset'
+      }
+
+      let app = document.getElementById('app')
+      if (app) {
+        app.style.height = '100%'
+        app.style.overflow = 'visible'
+        app.style.minHeight = '100vh'
+      }
+
+      let appBgWrapper = document.getElementById('app_bg_wrapper')
+      if (appBgWrapper) {
+        appBgWrapper.style.overflow = 'visible'
+      }
+
+      let main = document.getElementsByClassName('main')[0]
+      if (main) {
+        main.style.overflow = 'visible'
+        main.style.height = 'unset'
+      }
+
+      let content = document.getElementById('content')
+      if (content) {
+        content.style.paddingTop = '60px'
+        content.style.height = 'unset'
+        content.style.overflow = 'unset'
+      }
+    }
+  }
+}
+
+export default ChatLayout
diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js
new file mode 100644
index 00000000..f07ba2a1
--- /dev/null
+++ b/src/components/chat/chat_layout_utils.js
@@ -0,0 +1,27 @@
+// Captures a scroll position
+export const getScrollPosition = (el) => {
+  return {
+    scrollTop: el.scrollTop,
+    scrollHeight: el.scrollHeight,
+    offsetHeight: el.offsetHeight
+  }
+}
+
+// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
+// Takes two scroll positions, before and after the update.
+export const getNewTopPosition = (previousPosition, newPosition) => {
+  return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
+}
+
+export const isBottomedOut = (el, offset = 0) => {
+  if (!el) { return }
+  const scrollHeight = el.scrollTop + offset
+  const totalHeight = el.scrollHeight - el.offsetHeight
+  return totalHeight <= scrollHeight
+}
+
+// Height of the scrollable container. The dynamic height is needed to ensure the mobile browser panel doesn't overlap or hide the posting form.
+export const scrollableContainerHeight = (inner, header, footer) => {
+  const height = parseFloat(getComputedStyle(inner, null).height.replace('px', ''))
+  return height - header.clientHeight - footer.clientHeight
+}
diff --git a/src/components/chat_avatar/chat_avatar.js b/src/components/chat_avatar/chat_avatar.js
new file mode 100644
index 00000000..7b26e07c
--- /dev/null
+++ b/src/components/chat_avatar/chat_avatar.js
@@ -0,0 +1,23 @@
+import StillImage from '../still-image/still-image.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { mapState } from 'vuex'
+
+const ChatAvatar = {
+  props: ['user', 'width', 'height'],
+  components: {
+    StillImage
+  },
+  methods: {
+    getUserProfileLink (user) {
+      if (!user) { return }
+      return generateProfileLink(user.id, user.screen_name)
+    }
+  },
+  computed: {
+    ...mapState({
+      betterShadow: state => state.interface.browserSupport.cssFilter
+    })
+  }
+}
+
+export default ChatAvatar
diff --git a/src/components/chat_avatar/chat_avatar.vue b/src/components/chat_avatar/chat_avatar.vue
new file mode 100644
index 00000000..f54a7151
--- /dev/null
+++ b/src/components/chat_avatar/chat_avatar.vue
@@ -0,0 +1,53 @@
+<template>
+  <router-link
+    :to="getUserProfileLink(user) || ''"
+  >
+    <StillImage
+      v-if="user"
+      :style="{ 'width': width, 'height': height }"
+      class="avatar chat-avatar single-user"
+      :alt="user.screen_name"
+      :title="user.screen_name"
+      :src="user.profile_image_url_original"
+      error-src="/images/avi.png"
+      :class="{ 'better-shadow': betterShadow }"
+    />
+    <div
+      v-else
+      class="avatar chat-avatar single-user"
+      :style="{ 'width': width, 'height': height }"
+    />
+  </router-link>
+</template>
+
+<script src="./chat_avatar.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.chat-avatar {
+  display: inline-block;
+  vertical-align: middle;
+
+  &.single-user {
+    border-radius: $fallback--avatarAltRadius;
+    border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+  }
+
+  .avatar.still-image {
+    width: 48px;
+    height: 48px;
+
+    box-shadow: var(--avatarStatusShadow);
+    border-radius: 0;
+
+    &.better-shadow {
+      box-shadow: var(--avatarStatusShadowInset);
+      filter: var(--avatarStatusShadowFilter)
+    }
+
+    &.animated::before {
+      display: none;
+    }
+  }
+}
+</style>
diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js
new file mode 100644
index 00000000..95708d1d
--- /dev/null
+++ b/src/components/chat_list/chat_list.js
@@ -0,0 +1,37 @@
+import { mapState, mapGetters } from 'vuex'
+import ChatListItem from '../chat_list_item/chat_list_item.vue'
+import ChatNew from '../chat_new/chat_new.vue'
+import List from '../list/list.vue'
+
+const ChatList = {
+  components: {
+    ChatListItem,
+    List,
+    ChatNew
+  },
+  computed: {
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    }),
+    ...mapGetters(['sortedChatList'])
+  },
+  data () {
+    return {
+      isNew: false
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchChats', { latest: true })
+  },
+  methods: {
+    cancelNewChat () {
+      this.isNew = false
+      this.$store.dispatch('fetchChats', { latest: true })
+    },
+    newChat () {
+      this.isNew = true
+    }
+  }
+}
+
+export default ChatList
diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue
new file mode 100644
index 00000000..e62f58e5
--- /dev/null
+++ b/src/components/chat_list/chat_list.vue
@@ -0,0 +1,48 @@
+<template>
+  <div v-if="isNew">
+    <ChatNew @cancel="cancelNewChat" />
+  </div>
+  <div
+    v-else
+    class="chat-list panel panel-default"
+  >
+    <div class="panel-heading">
+      <span class="title">
+        {{ $t("chats.chats") }}
+      </span>
+      <button @click="newChat">
+        {{ $t("chats.new") }}
+      </button>
+    </div>
+    <div class="panel-body">
+      <div class="timeline">
+        <List :items="sortedChatList">
+          <template
+            slot="item"
+            slot-scope="{item}"
+          >
+            <ChatListItem
+              :key="item.id"
+              :compact="false"
+              :chat="item"
+            />
+          </template>
+        </List>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./chat_list.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.chat-list {
+  min-height: calc(100vh - 67px);
+  margin-bottom: 0;
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+</style>
diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js
new file mode 100644
index 00000000..1c27088c
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.js
@@ -0,0 +1,65 @@
+import { mapState } from 'vuex'
+import StatusContent from '../status_content/status_content.vue'
+import fileType from 'src/services/file_type/file_type.service'
+import ChatAvatar from '../chat_avatar/chat_avatar.vue'
+import AvatarList from '../avatar_list/avatar_list.vue'
+import Timeago from '../timeago/timeago.vue'
+import ChatTitle from '../chat_title/chat_title.vue'
+
+const ChatListItem = {
+  name: 'ChatListItem',
+  props: [
+    'chat'
+  ],
+  components: {
+    ChatAvatar,
+    AvatarList,
+    Timeago,
+    ChatTitle,
+    StatusContent
+  },
+  computed: {
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    }),
+    attachmentInfo () {
+      if (this.chat.lastMessage.attachments.length === 0) { return }
+
+      const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
+      if (types.includes('video')) {
+        return this.$t('file_type.video')
+      } else if (types.includes('audio')) {
+        return this.$t('file_type.audio')
+      } else if (types.includes('image')) {
+        return this.$t('file_type.image')
+      } else {
+        return this.$t('file_type.file')
+      }
+    },
+    messageForStatusContent () {
+      const content = this.chat.lastMessage ? (this.attachmentInfo || this.chat.lastMessage.content) : ''
+
+      return {
+        summary: '',
+        statusnet_html: content,
+        text: content,
+        attachments: []
+      }
+    }
+  },
+  methods: {
+    openChat (_e) {
+      if (this.chat.id) {
+        this.$router.push({
+          name: 'chat',
+          params: {
+            username: this.currentUser.screen_name,
+            recipient_id: this.chat.account.id
+          }
+        })
+      }
+    }
+  }
+}
+
+export default ChatListItem
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
new file mode 100644
index 00000000..12269f89
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -0,0 +1,94 @@
+.chat-list-item {
+  &:hover .animated.avatar {
+    canvas {
+      display: none;
+    }
+    img {
+      visibility: visible;
+    }
+  }
+
+  display: flex;
+  flex-direction: row;
+  padding: 0.75em;
+  height: 4.85em;
+  overflow: hidden;
+  box-sizing: border-box;
+  cursor: pointer;
+
+  :focus {
+    outline: none;
+  }
+
+  &:hover {
+    background-color: var(--selectedPost, $fallback--lightBg);
+    box-shadow: 0 0px 3px 1px rgba(0, 0, 0, 0.1);
+  }
+
+  .chat-list-item-left {
+    margin-right: 1em;
+  }
+
+  .chat-list-item-center {
+    width: 100%;
+    box-sizing: border-box;
+    overflow: hidden;
+    word-wrap: break-word;
+  }
+
+  .heading {
+    width: 100%;
+    display: inline-flex;
+    justify-content: space-between;
+    line-height: 1em;
+  }
+
+  .heading-right {
+    white-space: nowrap;
+  }
+
+  .member-count {
+    color: $fallback--text;
+    color: var(--faintText, $fallback--text);
+    margin-right: 2px;
+  }
+
+  .name-and-account-name {
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+    flex-shrink: 1;
+  }
+
+  .chat-preview {
+    display: inline-flex;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    margin: 0.35rem 0;
+    height: 1.2em;
+    line-height: 1.2em;
+    color: $fallback--text;
+    color: var(--faint, $fallback--text);
+  }
+
+  a {
+    color: var(--faintLink, $fallback--link);
+    text-decoration: none;
+    pointer-events: none;
+  }
+
+  .unread-indicator-wrapper {
+    display: flex;
+    align-items: center;
+    margin-left: 10px;
+  }
+
+  .unread-indicator {
+    border-radius: 100%;
+    height: 8px;
+    width: 8px;
+    background-color: $fallback--link;
+    background-color: var(--link, $fallback--link);
+  }
+}
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
new file mode 100644
index 00000000..26ad581b
--- /dev/null
+++ b/src/components/chat_list_item/chat_list_item.vue
@@ -0,0 +1,49 @@
+<template>
+  <div
+    class="chat-list-item"
+    @click.capture.prevent="openChat"
+  >
+    <div class="chat-list-item-left">
+      <ChatAvatar
+        :user="chat.account"
+        height="48px"
+        width="48px"
+      />
+    </div>
+    <div class="chat-list-item-center">
+      <div class="heading">
+        <span
+          v-if="chat.account"
+          class="name-and-account-name"
+        >
+          <ChatTitle
+            :user="chat.account"
+          />
+        </span>
+        <span class="heading-right" />
+      </div>
+      <div class="chat-preview">
+        <StatusContent :status="messageForStatusContent" />
+        <div
+          v-if="chat.unread > 0"
+          class="badge badge-notification unread-chat-count"
+        >
+          {{ chat.unread }}
+        </div>
+      </div>
+    </div>
+    <div>
+      <Timeago
+        :time="chat.updated_at"
+        :auto-update="60"
+      />
+    </div>
+  </div>
+</template>
+
+<script src="./chat_list_item.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+@import './chat_list_item.scss';
+</style>
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
new file mode 100644
index 00000000..aba95074
--- /dev/null
+++ b/src/components/chat_message/chat_message.js
@@ -0,0 +1,109 @@
+import { mapState, mapGetters } from 'vuex'
+import Popover from '../popover/popover.vue'
+import Attachment from '../attachment/attachment.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
+import Gallery from '../gallery/gallery.vue'
+import LinkPreview from '../link-preview/link-preview.vue'
+import StatusContent from '../status_content/status_content.vue'
+import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+
+const ChatMessage = {
+  name: 'ChatMessage',
+  props: [
+    'author',
+    'edited',
+    'noHeading',
+    'chatViewItem',
+    'hoveredMessageChain'
+  ],
+  components: {
+    Popover,
+    Attachment,
+    StatusContent,
+    UserAvatar,
+    Gallery,
+    LinkPreview,
+    ChatMessageDate
+  },
+  computed: {
+    // Returns HH:MM (hours and minutes) in local time.
+    createdAt () {
+      const time = this.chatViewItem.data.created_at
+      return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
+    },
+    isCurrentUser () {
+      return this.message.account_id === this.currentUser.id
+    },
+    message () {
+      return this.chatViewItem.data
+    },
+    userProfileLink () {
+      return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
+    },
+    isMessage () {
+      return this.chatViewItem.type === 'message'
+    },
+    messageForStatusContent () {
+      return {
+        summary: '',
+        statusnet_html: this.message.content,
+        text: this.message.content,
+        attachments: this.message.attachments
+      }
+    },
+    hasAttachment () {
+      return this.message.attachments.length > 0
+    },
+    ...mapState({
+      betterShadow: state => state.interface.browserSupport.cssFilter,
+      currentUser: state => state.users.currentUser,
+      restrictedNicknames: state => state.instance.restrictedNicknames
+    }),
+    ellipsisButtonWrapperStyle () {
+      let res = {
+        'opacity': this.hovered || this.menuOpened ? '1' : '0'
+      }
+
+      if (this.isCurrentUser) {
+        res.right = '5px'
+      } else {
+        res.left = '5px'
+      }
+
+      return res
+    },
+    popoverMarginStyle () {
+      if (this.isCurrentUser) {
+        return {}
+      } else {
+        return { left: 50 }
+      }
+    },
+    ...mapGetters(['mergedConfig', 'findUser'])
+  },
+  data () {
+    return {
+      hovered: false,
+      menuOpened: false
+    }
+  },
+  methods: {
+    onHover (bool) {
+      this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
+    },
+    async deleteMessage () {
+      const confirmed = window.confirm(this.$t('chats.delete_confirm'))
+      if (confirmed) {
+        await this.$store.dispatch('deleteChatMessage', {
+          messageId: this.chatViewItem.data.id,
+          chatId: this.chatViewItem.data.chat_id
+        })
+      }
+      this.hovered = false
+      this.menuOpened = false
+    }
+  }
+}
+
+export default ChatMessage
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
new file mode 100644
index 00000000..e4028537
--- /dev/null
+++ b/src/components/chat_message/chat_message.scss
@@ -0,0 +1,157 @@
+@import '../../_variables.scss';
+
+.chat-message-wrapper {
+  &.hovered-message-chain {
+    .animated.avatar {
+      canvas {
+        display: none;
+      }
+      img {
+        visibility: visible;
+      }
+    }
+  }
+
+  &:last-child {
+    margin-bottom: 16px;
+  }
+
+  .chat-message-menu {
+    transition: opacity 0.1s;
+    opacity: 0;
+    position: absolute;
+    top: -10px;
+
+    button {
+      padding-top: 3px;
+      padding-bottom: 3px;
+    }
+  }
+
+  .icon-ellipsis {
+    cursor: pointer;
+
+    &:hover, .extra-button-popover.open & {
+      color: $fallback--text;
+      color: var(--text, $fallback--text);
+    }
+
+    border-radius: $fallback--chatMessageRadius;
+    border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
+  }
+
+  .popover {
+    width: 12rem;
+  }
+
+  .chat-message {
+    display: flex;
+    padding-bottom: 7px;
+  }
+
+  .avatar-wrapper {
+    margin-right: 10px;
+    width: 32px;
+  }
+
+  .link-preview, .attachments {
+    margin-bottom: 0.9em;
+  }
+
+  .chat-message-inner {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    max-width: 80%;
+    min-width: 10rem;
+    width: 100%;
+
+    &.with-media {
+      width: 100%;
+
+      .gallery-row {
+        overflow: hidden;
+      }
+
+      .status {
+        width: 100%;
+      }
+    }
+  }
+
+  .status {
+    border-radius: $fallback--chatMessageRadius;
+    border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
+    display: flex;
+    padding: 0.75em;
+  }
+
+  .created-at {
+    float: right;
+    font-size: 0.8em;
+    margin: -10px 0 -5px 4px;
+    font-style: italic;
+    opacity: 0.8;
+  }
+
+  .without-attachment {
+    .status-content {
+      white-space: normal;
+
+      &::after {
+        margin-right: 75px;
+        content: " ";
+        display: inline-block;
+      }
+    }
+  }
+
+  .incoming {
+    a {
+      color: var(--chatMessageIncomingLink, $fallback--link);
+    }
+
+    .status {
+      color: var(--chatMessageIncomingText, $fallback--text);
+      background-color: var(--chatMessageIncomingBg, $fallback--bg);
+      border: 1px solid var(--chatMessageIncomingBorder, --border);
+    }
+
+    .created-at {
+      a {
+        color: var(--chatMessageIncomingText, $fallback--text);
+      }
+    }
+  }
+
+  .outgoing {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-content: end;
+    justify-content: flex-end;
+
+    a {
+      color: var(--chatMessageOutgoingLink, $fallback--link);
+    }
+
+    .status {
+      color: var(--chatMessageOutgoingText, $fallback--text);
+      background-color: var(--chatMessageOutgoingBg, $fallback--lightBg);
+      border: 1px solid var(--chatMessageOutgoingBorder, --lightBg);
+    }
+
+    .chat-message-inner {
+      align-items: flex-end;
+    }
+  }
+}
+
+.chat-message-date-separator {
+  text-align: center;
+  margin: 1.4em 0;
+  font-size: 0.9em;
+  user-select: none;
+  color: $fallback--text;
+  color: var(--faintedText, $fallback--text);
+}
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
new file mode 100644
index 00000000..872ddf70
--- /dev/null
+++ b/src/components/chat_message/chat_message.vue
@@ -0,0 +1,99 @@
+<template>
+  <div
+    v-if="isMessage"
+    class="chat-message-wrapper"
+    :class="{ 'hovered-message-chain': hoveredMessageChain }"
+    @mouseover="onHover(true)"
+    @mouseleave="onHover(false)"
+  >
+    <div
+      class="chat-message"
+      :class="[{ 'outgoing': isCurrentUser, 'incoming': !isCurrentUser }]"
+    >
+      <div
+        v-if="!isCurrentUser"
+        class="avatar-wrapper"
+      >
+        <router-link
+          v-if="chatViewItem.isHead"
+          :to="userProfileLink"
+        >
+          <UserAvatar
+            :compact="true"
+            :better-shadow="betterShadow"
+            :user="author"
+          />
+        </router-link>
+      </div>
+      <div class="chat-message-inner">
+        <div
+          class="status-body"
+          :style="{ 'min-width': message.attachment ? '80%' : '' }"
+        >
+          <div
+            class="media status"
+            :class="{ 'without-attachment': !hasAttachment }"
+            style="position: relative"
+            @mouseenter="hovered = true"
+            @mouseleave="hovered = false"
+          >
+            <div
+              class="chat-message-menu"
+              :style="ellipsisButtonWrapperStyle"
+            >
+              <Popover
+                trigger="click"
+                placement="top"
+                :bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
+                :bound-to="{ x: 'container' }"
+                :margin="popoverMarginStyle"
+                @show="menuOpened = true"
+                @close="menuOpened = false"
+              >
+                <div slot="content">
+                  <div class="dropdown-menu">
+                    <button
+                      class="dropdown-item dropdown-item-icon"
+                      @click="deleteMessage"
+                    >
+                      <i class="icon-cancel" /> {{ $t("chats.delete") }}
+                    </button>
+                  </div>
+                </div>
+                <button
+                  slot="trigger"
+                  :title="$t('chats.more')"
+                >
+                  <i class="icon-ellipsis" />
+                </button>
+              </Popover>
+            </div>
+            <StatusContent
+              :status="messageForStatusContent"
+              :full-content="true"
+            >
+              <span
+                slot="footer"
+                class="created-at"
+              >
+                {{ createdAt }}
+              </span>
+            </StatusContent>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div
+    v-else
+    class="chat-message-date-separator"
+  >
+    <ChatMessageDate :date="chatViewItem.date" />
+  </div>
+</template>
+
+<script src="./chat_message.js" ></script>
+<style lang="scss">
+@import './chat_message.scss';
+
+</style>
diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue
new file mode 100644
index 00000000..79c346b6
--- /dev/null
+++ b/src/components/chat_message_date/chat_message_date.vue
@@ -0,0 +1,24 @@
+<template>
+  <time>
+    {{ displayDate }}
+  </time>
+</template>
+
+<script>
+export default {
+  name: 'Timeago',
+  props: ['date'],
+  computed: {
+    displayDate () {
+      const today = new Date()
+      today.setHours(0, 0, 0, 0)
+
+      if (this.date.getTime() === today.getTime()) {
+        return this.$t('display_date.today')
+      } else {
+        return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
+      }
+    }
+  }
+}
+</script>
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
new file mode 100644
index 00000000..0da681f7
--- /dev/null
+++ b/src/components/chat_new/chat_new.js
@@ -0,0 +1,74 @@
+import { throttle } from 'lodash'
+import { mapState, mapGetters } from 'vuex'
+import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
+
+const chatNew = {
+  components: {
+    BasicUserCard,
+    UserAvatar
+  },
+  data () {
+    return {
+      suggestions: [],
+      userIds: [],
+      loading: false,
+      query: ''
+    }
+  },
+  async created () {
+    const { chats } = await this.backendInteractor.chats()
+    chats.forEach(chat => this.suggestions.push(chat.account))
+  },
+  computed: {
+    users () {
+      return this.userIds.map(userId => this.findUser(userId))
+    },
+    availableUsers () {
+      if (this.query.length !== 0) {
+        return this.users
+      } else {
+        return this.suggestions
+      }
+    },
+    ...mapState({
+      currentUser: state => state.users.currentUser,
+      backendInteractor: state => state.api.backendInteractor
+    }),
+    ...mapGetters(['findUser'])
+  },
+  methods: {
+    goBack () {
+      this.$emit('cancel')
+    },
+    goToChat (user) {
+      this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
+    },
+    onInput () {
+      this.search(this.query)
+    },
+    addUser (user) {
+      this.selectedUserIds.push(user.id)
+      this.query = ''
+    },
+    removeUser (userId) {
+      this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
+    },
+    search: throttle(function (query) {
+      if (!query) {
+        this.loading = false
+        return
+      }
+
+      this.loading = true
+      this.userIds = []
+      this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
+        .then(data => {
+          this.loading = false
+          this.userIds = data.accounts.map(a => a.id)
+        })
+    })
+  }
+}
+
+export default chatNew
diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss
new file mode 100644
index 00000000..39216677
--- /dev/null
+++ b/src/components/chat_new/chat_new.scss
@@ -0,0 +1,29 @@
+.chat-new {
+  .input-wrap {
+    display: flex;
+    margin: 0.7em 0.5em 0.7em 0.5em;
+
+    input {
+      width: 100%;
+    }
+  }
+
+  .icon-search {
+    font-size: 1.5em;
+    float: right;
+    margin-right: 0.3em;
+  }
+
+  .member-list {
+    padding-bottom: 0.67rem;
+  }
+
+  .basic-user-card:hover {
+    cursor: pointer;
+    background-color: var(--selectedPost, $fallback--lightBg);
+  }
+
+  .go-back-button {
+    cursor: pointer;
+  }
+}
diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue
new file mode 100644
index 00000000..3333dbf9
--- /dev/null
+++ b/src/components/chat_new/chat_new.vue
@@ -0,0 +1,46 @@
+<template>
+  <div
+    id="nav"
+    class="panel-default panel chat-new"
+  >
+    <div
+      ref="header"
+      class="panel-heading"
+    >
+      <a
+        class="go-back-button"
+        @click="goBack"
+      >
+        <i class="button-icon icon-left-open" />
+      </a>
+    </div>
+    <div class="input-wrap">
+      <div class="input-search">
+        <i class="button-icon icon-search" />
+      </div>
+      <input
+        ref="search"
+        v-model="query"
+        placeholder="Search people"
+        @input="onInput"
+      >
+    </div>
+    <div class="member-list">
+      <div
+        v-for="user in availableUsers"
+        :key="user.id"
+        class="member"
+      >
+        <div @click.capture.prevent="goToChat(user)">
+          <BasicUserCard :user="user" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./chat_new.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+@import './chat_new.scss';
+</style>
diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue
index 3677722f..12968cfb 100644
--- a/src/components/chat_panel/chat_panel.vue
+++ b/src/components/chat_panel/chat_panel.vue
@@ -84,54 +84,56 @@
   max-width: 25em;
 }
 
-.chat-heading {
-  cursor: pointer;
-  .icon-comment-empty {
-    color: $fallback--text;
-    color: var(--text, $fallback--text);
+.chat-panel {
+  .chat-heading {
+    cursor: pointer;
+    .icon-comment-empty {
+      color: $fallback--text;
+      color: var(--text, $fallback--text);
+    }
   }
-}
-
-.chat-window {
-  overflow-y: auto;
-  overflow-x: hidden;
-  max-height: 20em;
-}
 
-.chat-window-container {
-  height: 100%;
-}
+  .chat-window {
+    overflow-y: auto;
+    overflow-x: hidden;
+    max-height: 20em;
+  }
 
-.chat-message {
-  display: flex;
-  padding: 0.2em 0.5em
-}
+  .chat-window-container {
+    height: 100%;
+  }
 
-.chat-avatar {
-  img {
-    height: 24px;
-    width: 24px;
-    border-radius: $fallback--avatarRadius;
-    border-radius: var(--avatarRadius, $fallback--avatarRadius);
-    margin-right: 0.5em;
-    margin-top: 0.25em;
+  .chat-message {
+    display: flex;
+    padding: 0.2em 0.5em
   }
-}
 
-.chat-input {
-  display: flex;
-  textarea {
-    flex: 1;
-    margin: 0.6em;
-    min-height: 3.5em;
-    resize: none;
+  .chat-avatar {
+    img {
+      height: 24px;
+      width: 24px;
+      border-radius: $fallback--avatarRadius;
+      border-radius: var(--avatarRadius, $fallback--avatarRadius);
+      margin-right: 0.5em;
+      margin-top: 0.25em;
+    }
   }
-}
 
-.chat-panel {
-  .title {
+  .chat-input {
     display: flex;
-    justify-content: space-between;
+    textarea {
+      flex: 1;
+      margin: 0.6em;
+      min-height: 3.5em;
+      resize: none;
+    }
+  }
+
+  .chat-panel {
+    .title {
+      display: flex;
+      justify-content: space-between;
+    }
   }
 }
 </style>
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
new file mode 100644
index 00000000..2723d5f5
--- /dev/null
+++ b/src/components/chat_title/chat_title.js
@@ -0,0 +1,20 @@
+import Vue from 'vue'
+import ChatAvatar from '../chat_avatar/chat_avatar.vue'
+
+export default Vue.component('chat-title', {
+  name: 'ChatTitle',
+  components: {
+    ChatAvatar
+  },
+  props: [
+    'user', 'withAvatar'
+  ],
+  computed: {
+    title () {
+      return this.user ? this.user.screen_name : ''
+    },
+    htmlTitle () {
+      return this.user ? this.user.name_html : ''
+    }
+  }
+})
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
new file mode 100644
index 00000000..fd42d125
--- /dev/null
+++ b/src/components/chat_title/chat_title.vue
@@ -0,0 +1,56 @@
+<template>
+  <!-- eslint-disable vue/no-v-html -->
+  <div
+    class="chat-title"
+    :title="title"
+  >
+    <ChatAvatar
+      v-if="withAvatar"
+      :user="user"
+      width="23px"
+      height="23px"
+    />
+    <span
+      v-if="withAvatar"
+      style="margin-right: 0.5em"
+    />
+    <span
+      class="username"
+      v-html="htmlTitle"
+    />
+  </div>
+  <!-- eslint-enable vue/no-v-html -->
+</template>
+
+<script src="./chat_title.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.chat-title {
+  display: flex;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  a {
+    display: flex;
+    align-items: center;
+  }
+
+  .username {
+    max-width: 100%;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    display: inline;
+    word-wrap: break-word;
+
+    .emoji {
+      width: 14px;
+      height: 14px;
+      vertical-align: middle;
+      object-fit: contain
+    }
+  }
+}
+</style>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 7974a66d..a27da090 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -79,6 +79,15 @@ const EmojiInput = {
       required: false,
       type: Boolean,
       default: false
+    },
+    placement: {
+      /**
+       * Forces the panel to take a specific position relative to the input element.
+       * The 'auto' placement chooses either bottom or top depending on which has the available space (when both have available space, bottom is preferred).
+       */
+      required: false,
+      type: String, // 'auto', 'top', 'bottom'
+      default: 'auto'
     }
   },
   data () {
@@ -162,6 +171,11 @@ const EmojiInput = {
       input.elm.removeEventListener('input', this.onInput)
     }
   },
+  watch: {
+    showSuggestions: function (newValue) {
+      this.$emit('shown', newValue)
+    }
+  },
   methods: {
     triggerShowPicker () {
       this.showPicker = true
@@ -425,15 +439,29 @@ const EmojiInput = {
       this.caret = selectionStart
     },
     resize () {
-      const { panel, picker } = this.$refs
+      const panel = this.$refs.panel
       if (!panel) return
+      const picker = this.$refs.picker.$el
+      const panelBody = this.$refs['panel-body']
       const { offsetHeight, offsetTop } = this.input.elm
       const offsetBottom = offsetTop + offsetHeight
 
-      panel.style.top = offsetBottom + 'px'
-      if (!picker) return
-      picker.$el.style.top = offsetBottom + 'px'
-      picker.$el.style.bottom = 'auto'
+      this.setPlacement(panelBody, panel, offsetBottom)
+      this.setPlacement(picker, picker, offsetBottom)
+    },
+    setPlacement (container, target, offsetBottom) {
+      if (!container || !target) return
+
+      target.style.top = offsetBottom + 'px'
+      target.style.bottom = 'auto'
+
+      if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
+        target.style.top = 'auto'
+        target.style.bottom = this.input.elm.offsetHeight + 'px'
+      }
+    },
+    overflowsBottom (el) {
+      return el.getBoundingClientRect().bottom > window.innerHeight
     }
   }
 }
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index e9ac09c3..b9a74572 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -29,7 +29,10 @@
       class="autocomplete-panel"
       :class="{ hide: !showSuggestions }"
     >
-      <div class="autocomplete-panel-body">
+      <div
+        ref="panel-body"
+        class="autocomplete-panel-body"
+      >
         <div
           v-for="(suggestion, index) in suggestions"
           :key="index"
diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js
index 5f80a079..620a85ea 100644
--- a/src/components/features_panel/features_panel.js
+++ b/src/components/features_panel/features_panel.js
@@ -1,6 +1,7 @@
 const FeaturesPanel = {
   computed: {
     chat: function () { return this.$store.state.instance.chatAvailable },
+    pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
     gopher: function () { return this.$store.state.instance.gopherAvailable },
     whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
     mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue
index 3e5939a6..608b11c8 100644
--- a/src/components/features_panel/features_panel.vue
+++ b/src/components/features_panel/features_panel.vue
@@ -11,6 +11,9 @@
           <li v-if="chat">
             {{ $t('features_panel.chat') }}
           </li>
+          <li v-if="pleromaChatMessages">
+            {{ $t('features_panel.pleroma_chat_messages') }}
+          </li>
           <li v-if="gopher">
             {{ $t('features_panel.gopher') }}
           </li>
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index fbb2d03d..7b8a76cc 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -61,7 +61,8 @@ const mediaUpload = {
     }
   },
   props: [
-    'dropFiles'
+    'dropFiles',
+    'disabled'
   ],
   watch: {
     'dropFiles': function (fileInfos) {
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 5e31730b..d719eae1 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,5 +1,8 @@
 <template>
-  <div class="media-upload">
+  <div
+    class="media-upload"
+    :class="{ disabled: disabled }"
+  >
     <label
       class="label"
       :title="$t('tool_tip.media_upload')"
@@ -14,6 +17,7 @@
       />
       <input
         v-if="uploadReady"
+        :disabled="disabled"
         type="file"
         style="position: fixed; top: -100em"
         multiple="true"
@@ -26,7 +30,22 @@
 <script src="./media_upload.js" ></script>
 
 <style lang="scss">
+@import '../../_variables.scss';
+
 .media-upload {
+  &.disabled {
+    .new-icon {
+      cursor: not-allowed;
+    }
+
+    &:hover {
+      i, label {
+        color: $fallback--faint;
+        color: var(--faint, $fallback--faint);
+      }
+    }
+  }
+
   .label {
     display: inline-block;
   }
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index c1166a0c..b27ca6f4 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -30,7 +30,10 @@ const MobileNav = {
       return this.unseenNotifications.length
     },
     hideSitename () { return this.$store.state.instance.hideSitename },
-    sitename () { return this.$store.state.instance.name }
+    sitename () { return this.$store.state.instance.name },
+    navBarStyle () {
+      return { 'visibility': this.$route.name === 'chat' ? 'hidden' : 'visible' }
+    }
   },
   methods: {
     toggleMobileSidebar () {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 51f1d636..05568b90 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -3,6 +3,7 @@
     <nav
       id="nav"
       class="nav-bar container"
+      :style="navBarStyle"
     >
       <div
         class="mobile-inner-nav"
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index 0ad12bb1..6348277b 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -1,5 +1,10 @@
 import { debounce } from 'lodash'
 
+const HIDDEN_FOR_PAGES = new Set([
+  'chats',
+  'chat'
+])
+
 const MobilePostStatusButton = {
   data () {
     return {
@@ -27,6 +32,8 @@ const MobilePostStatusButton = {
       return !!this.$store.state.users.currentUser
     },
     isHidden () {
+      if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true }
+
       return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
     },
     autohideFloatingPostButton () {
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index 8f7edb7f..b142ffe0 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -1,4 +1,4 @@
-import { mapState } from 'vuex'
+import { mapState, mapGetters } from 'vuex'
 
 const NavPanel = {
   created () {
@@ -6,13 +6,17 @@ const NavPanel = {
       this.$store.dispatch('startFetchingFollowRequests')
     }
   },
-  computed: mapState({
-    currentUser: state => state.users.currentUser,
-    chat: state => state.chat.channel,
-    followRequestCount: state => state.api.followRequests.length,
-    privateMode: state => state.instance.private,
-    federating: state => state.instance.federating
-  })
+  computed: {
+    ...mapState({
+      currentUser: state => state.users.currentUser,
+      chat: state => state.chat.channel,
+      followRequestCount: state => state.api.followRequests.length,
+      privateMode: state => state.instance.private,
+      federating: state => state.instance.federating,
+      pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
+    }),
+    ...mapGetters(['unreadChatCount'])
+  }
 }
 
 export default NavPanel
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index f164b2b0..8a213d7e 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -22,6 +22,17 @@
             <i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
           </router-link>
         </li>
+        <li v-if="currentUser && pleromaChatMessagesAvailable">
+          <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }">
+            <div
+              v-if="unreadChatCount"
+              class="badge badge-notification unread-chat-count"
+            >
+              {{ unreadChatCount }}
+            </div>
+            <i class="button-icon icon-chat" /> {{ $t("nav.chats") }}
+          </router-link>
+        </li>
         <li v-if="currentUser && currentUser.locked">
           <router-link :to="{ name: 'friend-requests' }">
             <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 5aa40e98..bb906b50 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,4 +1,5 @@
 import StatusContent from '../status_content/status_content.vue'
+import { mapState } from 'vuex'
 import Status from '../status/status.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import UserCard from '../user_card/user_card.vue'
@@ -81,7 +82,10 @@ const Notification = {
     },
     isStatusNotification () {
       return isStatusNotification(this.notification.type)
-    }
+    },
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    })
   }
 }
 
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index d8a327b0..d951e2a8 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -1,3 +1,4 @@
+import { mapGetters } from 'vuex'
 import Notification from '../notification/notification.vue'
 import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
 import {
@@ -51,18 +52,22 @@ const Notifications = {
     unseenCount () {
       return this.unseenNotifications.length
     },
+    unseenCountTitle () {
+      return this.unseenCount + (this.unreadChatCount)
+    },
     loading () {
       return this.$store.state.statuses.notifications.loading
     },
     notificationsToDisplay () {
       return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
-    }
+    },
+    ...mapGetters(['unreadChatCount'])
   },
   components: {
     Notification
   },
   watch: {
-    unseenCount (count) {
+    unseenCountTitle (count) {
       if (count > 0) {
         this.$store.dispatch('setPageTitle', `(${count})`)
       } else {
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 18f02eba..90d0fa81 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -9,7 +9,7 @@ import fileTypeService from '../../services/file_type/file_type.service.js'
 import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
 import { reject, map, uniqBy, debounce } from 'lodash'
 import suggestor from '../emoji_input/suggestor.js'
-import { mapGetters } from 'vuex'
+import { mapGetters, mapState } from 'vuex'
 import Checkbox from '../checkbox/checkbox.vue'
 
 const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
@@ -33,7 +33,22 @@ const PostStatusForm = {
     'repliedUser',
     'attentions',
     'copyMessageScope',
-    'subject'
+    'subject',
+    'disableSubject',
+    'disableScopeSelector',
+    'disableNotice',
+    'disableLockWarning',
+    'disablePolls',
+    'disableSensitivityCheckbox',
+    'disableSubmit',
+    'placeholder',
+    'maxHeight',
+    'request',
+    'preserveFocus',
+    'autoFocus',
+    'fileLimit',
+    'submitOnEnter',
+    'emojiPickerPlacement'
   ],
   components: {
     MediaUpload,
@@ -46,10 +61,13 @@ const PostStatusForm = {
   },
   mounted () {
     this.resize(this.$refs.textarea)
-    const textLength = this.$refs.textarea.value.length
-    this.$refs.textarea.setSelectionRange(textLength, textLength)
 
     if (this.replyTo) {
+      const textLength = this.$refs.textarea.value.length
+      this.$refs.textarea.setSelectionRange(textLength, textLength)
+    }
+
+    if (this.replyTo || this.autoFocus) {
       this.$refs.textarea.focus()
     }
   },
@@ -72,7 +90,7 @@ const PostStatusForm = {
 
     return {
       dropFiles: [],
-      submitDisabled: false,
+      uploadingFiles: false,
       error: null,
       posting: false,
       highlighted: 0,
@@ -91,7 +109,8 @@ const PostStatusForm = {
       showDropIcon: 'hide',
       dropStopTimeout: null,
       preview: null,
-      previewLoading: false
+      previewLoading: false,
+      emojiInputShown: false
     }
   },
   computed: {
@@ -160,10 +179,11 @@ const PostStatusForm = {
     },
     pollsAvailable () {
       return this.$store.state.instance.pollsAvailable &&
-        this.$store.state.instance.pollLimits.max_options >= 2
+        this.$store.state.instance.pollLimits.max_options >= 2 &&
+        this.disablePolls !== true
     },
     hideScopeNotice () {
-      return this.$store.getters.mergedConfig.hideScopeNotice
+      return this.disableNotice || this.$store.getters.mergedConfig.hideScopeNotice
     },
     pollContentError () {
       return this.pollFormVisible &&
@@ -176,7 +196,13 @@ const PostStatusForm = {
     emptyStatus () {
       return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
     },
-    ...mapGetters(['mergedConfig'])
+    uploadFileLimitReached () {
+      return this.newStatus.files.length >= this.fileLimit
+    },
+    ...mapGetters(['mergedConfig']),
+    ...mapState({
+      mobileLayout: state => state.interface.mobileLayout
+    })
   },
   watch: {
     'newStatus.contentType': function () {
@@ -187,9 +213,19 @@ const PostStatusForm = {
     }
   },
   methods: {
-    async postStatus (newStatus) {
+    async postStatus (event, newStatus, opts = {}) {
       if (this.posting) { return }
       if (this.submitDisabled) { return }
+      if (this.emojiInputShown) { return }
+      if (this.submitOnEnter) {
+        event.stopPropagation()
+        event.preventDefault()
+      }
+      if (opts.control && this.submitOnEnter) {
+        newStatus.status = `${newStatus.status}\n`
+        return
+      }
+
       if (this.emptyStatus) {
         this.error = this.$t('post_status.empty_status_error')
         return
@@ -211,7 +247,7 @@ const PostStatusForm = {
         return
       }
 
-      const data = await statusPoster.postStatus({
+      const postingOptions = {
         status: newStatus.status,
         spoilerText: newStatus.spoilerText || null,
         visibility: newStatus.visibility,
@@ -221,32 +257,40 @@ const PostStatusForm = {
         inReplyToStatusId: this.replyTo,
         contentType: newStatus.contentType,
         poll
-      })
-
-      if (!data.error) {
-        this.newStatus = {
-          status: '',
-          spoilerText: '',
-          files: [],
-          visibility: newStatus.visibility,
-          contentType: newStatus.contentType,
-          poll: {},
-          mediaDescriptions: {}
-        }
-        this.pollFormVisible = false
-        this.$refs.mediaUpload.clearFile()
-        this.clearPollForm()
-        this.$emit('posted')
-        let el = this.$el.querySelector('textarea')
-        el.style.height = 'auto'
-        el.style.height = undefined
-        this.error = null
-        if (this.preview) this.previewStatus()
-      } else {
-        this.error = data.error
       }
 
-      this.posting = false
+      const request = this.request ? this.request : statusPoster.postStatus
+
+      request(postingOptions).then((data) => {
+        if (!data.error) {
+          this.newStatus = {
+            status: '',
+            spoilerText: '',
+            files: [],
+            visibility: newStatus.visibility,
+            contentType: newStatus.contentType,
+            poll: {},
+            mediaDescriptions: {}
+          }
+          this.pollFormVisible = false
+          this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
+          this.clearPollForm()
+          this.$emit('posted', data)
+          if (this.preserveFocus) {
+            this.$nextTick(() => {
+              this.$refs.textarea.focus()
+            })
+          }
+          let el = this.$el.querySelector('textarea')
+          el.style.height = 'auto'
+          el.style.height = undefined
+          this.error = null
+          if (this.preview) this.previewStatus()
+        } else {
+          this.error = data.error
+        }
+        this.posting = false
+      })
     },
     previewStatus () {
       if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
@@ -301,20 +345,26 @@ const PostStatusForm = {
     },
     addMediaFile (fileInfo) {
       this.newStatus.files.push(fileInfo)
+
+      // TODO: use fixed dimensions instead so relying on timeout
+      setTimeout(() => {
+        this.$emit('resize')
+      }, 150)
     },
     removeMediaFile (fileInfo) {
       let index = this.newStatus.files.indexOf(fileInfo)
       this.newStatus.files.splice(index, 1)
+      this.$emit('resize')
     },
     uploadFailed (errString, templateArgs) {
       templateArgs = templateArgs || {}
       this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
     },
-    disableSubmit () {
-      this.submitDisabled = true
+    startedUploadingFiles () {
+      this.uploadingFiles = true
     },
-    enableSubmit () {
-      this.submitDisabled = false
+    finishedUploadingFiles () {
+      this.uploadingFiles = false
     },
     type (fileInfo) {
       return fileTypeService.fileType(fileInfo.mimetype)
@@ -348,7 +398,7 @@ const PostStatusForm = {
       this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
     },
     fileDrag (e) {
-      e.dataTransfer.dropEffect = 'copy'
+      e.dataTransfer.dropEffect = this.uploadFileLimitReached ? 'none' : 'copy'
       if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
         clearTimeout(this.dropStopTimeout)
         this.showDropIcon = 'show'
@@ -367,6 +417,7 @@ const PostStatusForm = {
       // Reset to default height for empty form, nothing else to do here.
       if (target.value === '') {
         target.style.height = null
+        this.$emit('resize', null)
         this.$refs['emoji-input'].resize()
         return
       }
@@ -419,8 +470,10 @@ const PostStatusForm = {
 
       // BEGIN content size update
       target.style.height = 'auto'
-      const newHeight = target.scrollHeight - vertPadding
+      const heightWithoutPadding = target.scrollHeight - vertPadding
+      const newHeight = this.maxHeight ? Math.min(heightWithoutPadding, this.maxHeight) : heightWithoutPadding
       target.style.height = `${newHeight}px`
+      this.$emit('resize', newHeight)
       // END content size update
 
       // We check where the bottom border of form-bottom element is, this uses findOffset
@@ -480,6 +533,9 @@ const PostStatusForm = {
     setAllMediaDescriptions () {
       const ids = this.newStatus.files.map(file => file.id)
       return Promise.all(ids.map(id => this.setMediaDescription(id)))
+    },
+    handleEmojiInputShow (value) {
+      this.emojiInputShown = value
     }
   }
 }
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 626584ed..d8df68d6 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -5,19 +5,20 @@
   >
     <form
       autocomplete="off"
-      @submit.prevent="postStatus(newStatus)"
+      @submit.prevent
       @dragover.prevent="fileDrag"
     >
       <div
         v-show="showDropIcon !== 'hide'"
         :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
-        class="drop-indicator icon-upload"
+        class="drop-indicator"
+        :class="[uploadFileLimitReached ? 'icon-block' : 'icon-upload']"
         @dragleave="fileDragStop"
         @drop.stop="fileDrop"
       />
       <div class="form-group">
         <i18n
-          v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
+          v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning"
           path="post_status.account_not_locked_warning"
           tag="p"
           class="visibility-notice"
@@ -108,7 +109,7 @@
           />
         </div>
         <EmojiInput
-          v-if="newStatus.spoilerText || alwaysShowSubject"
+          v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
           v-model="newStatus.spoilerText"
           enable-emoji-picker
           :suggest="emojiSuggestor"
@@ -126,6 +127,7 @@
           ref="emoji-input"
           v-model="newStatus.status"
           :suggest="emojiUserSuggestor"
+          :placement="emojiPickerPlacement"
           class="form-control main-input"
           enable-emoji-picker
           hide-emoji-button
@@ -133,16 +135,19 @@
           @input="onEmojiInputInput"
           @sticker-uploaded="addMediaFile"
           @sticker-upload-failed="uploadFailed"
+          @shown="handleEmojiInputShow"
         >
           <textarea
             ref="textarea"
             v-model="newStatus.status"
-            :placeholder="$t('post_status.default')"
+            :placeholder="placeholder || $t('post_status.default')"
             rows="1"
             :disabled="posting"
             class="form-post-body"
-            @keydown.meta.enter="postStatus(newStatus)"
-            @keydown.ctrl.enter="postStatus(newStatus)"
+            :class="{ 'scrollable-form': !!maxHeight }"
+            @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
+            @keydown.meta.enter="postStatus($event, newStatus, { control: true })"
+            @keydown.ctrl.enter="postStatus($event, newStatus)"
             @input="resize"
             @compositionupdate="resize"
             @paste="paste"
@@ -155,7 +160,10 @@
             {{ charactersLeft }}
           </p>
         </EmojiInput>
-        <div class="visibility-tray">
+        <div
+          v-if="!disableScopeSelector"
+          class="visibility-tray"
+        >
           <scope-selector
             :show-all="showAllScopes"
             :user-default="userDefaultScope"
@@ -213,10 +221,11 @@
             ref="mediaUpload"
             class="media-upload-icon"
             :drop-files="dropFiles"
-            @uploading="disableSubmit"
+            :disabled="uploadFileLimitReached"
+            @uploading="startedUploadingFiles"
             @uploaded="addMediaFile"
             @upload-failed="uploadFailed"
-            @all-uploaded="enableSubmit"
+            @all-uploaded="finishedUploadingFiles"
           />
           <div
             class="emoji-icon"
@@ -253,11 +262,13 @@
         >
           {{ $t('general.submit') }}
         </button>
+        <!-- touchstart is used to keep the OSK at the same position after a message send -->
         <button
           v-else
-          :disabled="submitDisabled"
-          type="submit"
+          :disabled="uploadingFiles || disableSubmit"
           class="btn btn-default"
+          @touchstart.stop.prevent="postStatus($event, newStatus)"
+          @click.stop.prevent="postStatus($event, newStatus)"
         >
           {{ $t('general.submit') }}
         </button>
@@ -297,7 +308,7 @@
         </div>
       </div>
       <div
-        v-if="newStatus.files.length > 0"
+        v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
         class="upload_settings"
       >
         <Checkbox v-model="newStatus.nsfw">
@@ -331,6 +342,8 @@
 }
 
 .post-status-form {
+  position: relative;
+
   .form-bottom {
     display: flex;
     justify-content: space-between;
@@ -547,6 +560,10 @@
     padding-bottom: 1.75em;
     min-height: 1px;
     box-sizing: content-box;
+
+    &.scrollable-form {
+      overflow-y: auto;
+    }
   }
 
   .main-input {
@@ -609,4 +626,11 @@
     border: 2px dashed var(--text, $fallback--text);
   }
 }
+
+// todo: unify with attachment.vue (otherwise the uploaded images are not minified unless a status with an attachment was displayed before)
+img.media-upload {
+  line-height: 0;
+  max-height: 200px;
+  max-width: 100%;
+}
 </style>
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 9d61b0c4..e3c5e80a 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -99,7 +99,8 @@ export default {
       avatarRadiusLocal: '',
       avatarAltRadiusLocal: '',
       attachmentRadiusLocal: '',
-      tooltipRadiusLocal: ''
+      tooltipRadiusLocal: '',
+      chatMessageRadiusLocal: ''
     }
   },
   created () {
@@ -214,7 +215,8 @@ export default {
         avatar: this.avatarRadiusLocal,
         avatarAlt: this.avatarAltRadiusLocal,
         tooltip: this.tooltipRadiusLocal,
-        attachment: this.attachmentRadiusLocal
+        attachment: this.attachmentRadiusLocal,
+        chatMessage: this.chatMessageRadiusLocal
       }
     },
     preview () {
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index d14f854c..d57894de 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -735,6 +735,65 @@
             />
             <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
           </div>
+          <div class="color-item">
+            <h4>{{ $t('chats.chats') }}</h4>
+            <ColorInput
+              v-model="chatBgColorLocal"
+              name="chatBgColor"
+              :fallback="previewTheme.colors.bg || 1"
+              :label="$t('settings.background')"
+            />
+            <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5>
+            <ColorInput
+              v-model="chatMessageIncomingBgColorLocal"
+              name="chatMessageIncomingBgColor"
+              :fallback="previewTheme.colors.bg || 1"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="chatMessageIncomingTextColorLocal"
+              name="chatMessageIncomingTextColor"
+              :fallback="previewTheme.colors.text || 1"
+              :label="$t('settings.text')"
+            />
+            <ColorInput
+              v-model="chatMessageIncomingLinkColorLocal"
+              name="chatMessageIncomingLinkColor"
+              :fallback="previewTheme.colors.link || 1"
+              :label="$t('settings.links')"
+            />
+            <ColorInput
+              v-model="chatMessageIncomingBorderColorLocal"
+              name="chatMessageIncomingBorderLinkColor"
+              :fallback="previewTheme.colors.fg || 1"
+              :label="$t('settings.style.advanced_colors.chat.border')"
+            />
+            <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5>
+            <ColorInput
+              v-model="chatMessageOutgoingBgColorLocal"
+              name="chatMessageOutgoingBgColor"
+              :fallback="previewTheme.colors.bg || 1"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="chatMessageOutgoingTextColorLocal"
+              name="chatMessageOutgoingTextColor"
+              :fallback="previewTheme.colors.text || 1"
+              :label="$t('settings.text')"
+            />
+            <ColorInput
+              v-model="chatMessageOutgoingLinkColorLocal"
+              name="chatMessageOutgoingLinkColor"
+              :fallback="previewTheme.colors.link || 1"
+              :label="$t('settings.links')"
+            />
+            <ColorInput
+              v-model="chatMessageOutgoingBorderColorLocal"
+              name="chatMessageOutgoingBorderLinkColor"
+              :fallback="previewTheme.colors.bg || 1"
+              :label="$t('settings.style.advanced_colors.chat.border')"
+            />
+          </div>
         </div>
 
         <div
@@ -814,6 +873,14 @@
             max="50"
             hard-min="0"
           />
+          <RangeInput
+            v-model="chatMessageRadiusLocal"
+            name="chatMessageRadius"
+            :label="$t('settings.chatMessageRadius')"
+            :fallback="previewTheme.radii.chatMessage || 2"
+            max="50"
+            hard-min="0"
+          />
         </div>
 
         <div
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index d1f044f6..3a9e9e8f 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -1,3 +1,4 @@
+import { mapState, mapGetters } from 'vuex'
 import UserCard from '../user_card/user_card.vue'
 import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
 import GestureService from '../../services/gesture_service/gesture_service'
@@ -47,7 +48,11 @@ const SideDrawer = {
     },
     federating () {
       return this.$store.state.instance.federating
-    }
+    },
+    ...mapState({
+      pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
+    }),
+    ...mapGetters(['unreadChatCount'])
   },
   methods: {
     toggleDrawer () {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 0ac53b34..4fdb3d13 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -40,12 +40,24 @@
           </router-link>
         </li>
         <li
-          v-if="currentUser"
+          v-if="currentUser && pleromaChatMessagesAvailable"
           @click="toggleDrawer"
         >
           <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
             <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
           </router-link>
+          <router-link
+            :to="{ name: 'chats', params: { username: currentUser.screen_name } }"
+            style="position: relative"
+          >
+            <i class="button-icon icon-chat" /> {{ $t("nav.chats") }}
+            <span
+              v-if="unreadChatCount"
+              class="badge badge-notification unread-chat-count"
+            >
+              {{ unreadChatCount }}
+            </span>
+          </router-link>
         </li>
         <li
           v-if="currentUser"
@@ -103,14 +115,6 @@
             <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
           </router-link>
         </li>
-        <li
-          v-if="currentUser && chat"
-          @click="toggleDrawer"
-        >
-          <router-link :to="{ name: 'chat' }">
-            <i class="button-icon icon-chat" /> {{ $t("nav.chat") }}
-          </router-link>
-        </li>
       </ul>
       <ul>
         <li
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 67d9bd3c..dc83d8cb 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -18,7 +18,7 @@ const StatusContent = {
   ],
   data () {
     return {
-      showingTall: this.inConversation && this.focused,
+      showingTall: this.fullContent || (this.inConversation && this.focused),
       showingLongSubject: false,
       // not as computed because it sets the initial state which will be changed later
       expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 8068d8d2..9ca2cf6d 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -76,7 +76,7 @@
         />
       </a>
       <a
-        v-if="showingMore"
+        v-if="showingMore && !fullContent"
         href="#"
         class="status-unhider"
         @click.prevent="toggleShowMore"
diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss
index 4cefe2be..1a26eb8d 100644
--- a/src/hocs/with_load_more/with_load_more.scss
+++ b/src/hocs/with_load_more/with_load_more.scss
@@ -12,5 +12,9 @@
     .error {
       font-size: 14px;
     }
+
+    a {
+      cursor: pointer;
+    }
   }
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 72c3b1f7..c9a34556 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -44,6 +44,7 @@
   },
   "features_panel": {
     "chat": "Chat",
+    "pleroma_chat_messages": "Pleroma Chat",
     "gopher": "Gopher",
     "media_proxy": "Media proxy",
     "scope_options": "Scope options",
@@ -124,7 +125,8 @@
     "user_search": "User Search",
     "search": "Search",
     "who_to_follow": "Who to follow",
-    "preferences": "Preferences"
+    "preferences": "Preferences",
+    "chats": "Chats"
   },
   "notifications": {
     "broken_favorite": "Unknown status, searching for it…",
@@ -287,6 +289,7 @@
     "change_password": "Change Password",
     "change_password_error": "There was an issue changing your password.",
     "changed_password": "Password changed successfully!",
+    "chatMessageRadius": "Chat message",
     "collapse_subject": "Collapse posts with subjects",
     "composing": "Composing",
     "confirm_new_password": "Confirm new password",
@@ -518,7 +521,12 @@
         "selectedMenu": "Selected menu item",
         "disabled": "Disabled",
         "toggled": "Toggled",
-        "tabs": "Tabs"
+        "tabs": "Tabs",
+        "chat": {
+          "incoming": "Incoming",
+          "outgoing": "Outgoing",
+          "border": "Border"
+        }
       },
       "radii": {
         "_tab_label": "Roundness"
@@ -677,6 +685,7 @@
     "its_you": "It's you!",
     "media": "Media",
     "mention": "Mention",
+    "message": "Message",
     "mute": "Mute",
     "muted": "Muted",
     "per_day": "per day",
@@ -775,5 +784,25 @@
     "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
     "password_reset_required": "You must reset your password to log in.",
     "password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
+  },
+  "chats": {
+    "message_user": "Message {nickname}",
+    "delete": "Delete",
+    "chats": "Chats",
+    "new": "New Chat",
+    "empty_message_error": "Cannot post empty message",
+    "more": "More",
+    "delete_confirm": "Do you really want to delete this message?",
+    "error_loading_chat": "Something went wrong when loading the chat.",
+    "error_sending_message": "Something went wrong when sending the message."
+  },
+  "file_type": {
+    "audio": "Audio",
+    "video": "Video",
+    "image": "Image",
+    "file": "File"
+  },
+  "display_date": {
+    "today": "Today"
   }
 }
diff --git a/src/main.js b/src/main.js
index 5bddc76e..0a898022 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,6 +19,7 @@ import oauthTokensModule from './modules/oauth_tokens.js'
 import reportsModule from './modules/reports.js'
 import pollsModule from './modules/polls.js'
 import postStatusModule from './modules/postStatus.js'
+import chatsModule from './modules/chats.js'
 
 import VueI18n from 'vue-i18n'
 
@@ -91,7 +92,8 @@ const persistedStateOptions = {
       oauthTokens: oauthTokensModule,
       reports: reportsModule,
       polls: pollsModule,
-      postStatus: postStatusModule
+      postStatus: postStatusModule,
+      chats: chatsModule
     },
     plugins,
     strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/api.js b/src/modules/api.js
index 04ef6ab4..68402602 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,4 +1,5 @@
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import { WSConnectionStatus } from '../services/api/api.service.js'
 import { Socket } from 'phoenix'
 
 const api = {
@@ -7,6 +8,7 @@ const api = {
     fetchers: {},
     socket: null,
     mastoUserSocket: null,
+    mastoUserSocketStatus: null,
     followRequests: []
   },
   mutations: {
@@ -28,6 +30,9 @@ const api = {
     },
     setFollowRequests (state, value) {
       state.followRequests = value
+    },
+    setMastoUserSocketStatus (state, value) {
+      state.mastoUserSocketStatus = value
     }
   },
   actions: {
@@ -47,7 +52,7 @@ const api = {
     startMastoUserSocket (store) {
       return new Promise((resolve, reject) => {
         try {
-          const { state, dispatch, rootState } = store
+          const { state, commit, dispatch, rootState } = store
           const timelineData = rootState.statuses.timelines.friends
           state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
           state.mastoUserSocket.addEventListener(
@@ -66,11 +71,22 @@ const api = {
                   showImmediately: timelineData.visibleStatuses.length === 0,
                   timeline: 'friends'
                 })
+              } else if (message.event === 'pleroma:chat_update') {
+                dispatch('addChatMessages', {
+                  chatId: message.chatUpdate.id,
+                  messages: [message.chatUpdate.lastMessage]
+                })
+                dispatch('updateChat', { chat: message.chatUpdate })
               }
             }
           )
+          state.mastoUserSocket.addEventListener('open', () => {
+            commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
+          })
           state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
             console.error('Error in MastoAPI websocket:', error)
+            commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
+            dispatch('clearOpenedChats')
           })
           state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
             const ignoreCodes = new Set([
@@ -84,8 +100,11 @@ const api = {
               console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
               dispatch('startFetchingTimeline', { timeline: 'friends' })
               dispatch('startFetchingNotifications')
+              dispatch('startFetchingChats')
               dispatch('restartMastoUserSocket')
             }
+            commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
+            dispatch('clearOpenedChats')
           })
           resolve()
         } catch (e) {
@@ -99,12 +118,13 @@ const api = {
       return dispatch('startMastoUserSocket').then(() => {
         dispatch('stopFetchingTimeline', { timeline: 'friends' })
         dispatch('stopFetchingNotifications')
+        dispatch('stopFetchingChats')
       })
     },
     stopMastoUserSocket ({ state, dispatch }) {
       dispatch('startFetchingTimeline', { timeline: 'friends' })
       dispatch('startFetchingNotifications')
-      console.log(state.mastoUserSocket)
+      dispatch('startFetchingChats')
       state.mastoUserSocket.close()
     },
 
diff --git a/src/modules/chats.js b/src/modules/chats.js
new file mode 100644
index 00000000..f868ca0c
--- /dev/null
+++ b/src/modules/chats.js
@@ -0,0 +1,228 @@
+import Vue from 'vue'
+import { find, omitBy, orderBy, sumBy } from 'lodash'
+import chatService from '../services/chat_service/chat_service.js'
+import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
+
+const emptyChatList = () => ({
+  data: [],
+  idStore: {}
+})
+
+const defaultState = {
+  chatList: emptyChatList(),
+  chatListFetcher: null,
+  openedChats: {},
+  openedChatMessageServices: {},
+  fetcher: undefined,
+  currentChatId: null
+}
+
+const getChatById = (state, id) => {
+  return find(state.chatList.data, { id })
+}
+
+const sortedChatList = (state) => {
+  return orderBy(state.chatList.data, ['updated_at'], ['desc'])
+}
+
+const unreadChatCount = (state) => {
+  return sumBy(state.chatList.data, 'unread')
+}
+
+const chats = {
+  state: { ...defaultState },
+  getters: {
+    currentChat: state => state.openedChats[state.currentChatId],
+    currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
+    findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
+    sortedChatList,
+    unreadChatCount
+  },
+  actions: {
+    // Chat list
+    startFetchingChats ({ dispatch, commit }) {
+      const fetcher = () => {
+        dispatch('fetchChats', { latest: true })
+      }
+      fetcher()
+      commit('setChatListFetcher', {
+        fetcher: () => setInterval(() => { fetcher() }, 5000)
+      })
+    },
+    stopFetchingChats ({ commit }) {
+      commit('setChatListFetcher', { fetcher: undefined })
+    },
+    fetchChats ({ dispatch, rootState, commit }, params = {}) {
+      return rootState.api.backendInteractor.chats()
+        .then(({ chats }) => {
+          dispatch('addNewChats', { chats })
+          return chats
+        })
+    },
+    addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
+      commit('addNewChats', { dispatch, chats, rootGetters })
+    },
+    updateChat ({ commit }, { chat }) {
+      commit('updateChat', { chat })
+    },
+
+    // Opened Chats
+    startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
+      dispatch('setCurrentChatFetcher', { fetcher })
+    },
+    setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
+      commit('setCurrentChatFetcher', { fetcher })
+    },
+    addOpenedChat ({ rootState, commit, dispatch }, { chat }) {
+      commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
+      dispatch('addNewUsers', [chat.account])
+    },
+    addChatMessages ({ commit }, value) {
+      commit('addChatMessages', { commit, ...value })
+    },
+    resetChatNewMessageCount ({ commit }, value) {
+      commit('resetChatNewMessageCount', value)
+    },
+    removeFromCurrentChatStatuses ({ commit }, { id }) {
+      commit('removeFromCurrentChatStatuses', id)
+    },
+    clearCurrentChat ({ rootState, commit, dispatch }, value) {
+      commit('setCurrentChatId', { chatId: undefined })
+      commit('setCurrentChatFetcher', { fetcher: undefined })
+    },
+    readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
+      dispatch('resetChatNewMessageCount')
+      commit('readChat', { id })
+      rootState.api.backendInteractor.readChat({ id, lastReadId })
+    },
+    deleteChatMessage ({ rootState, commit }, value) {
+      rootState.api.backendInteractor.deleteChatMessage(value)
+      commit('deleteChatMessage', { commit, ...value })
+    },
+    resetChats ({ commit, dispatch }) {
+      dispatch('clearCurrentChat')
+      commit('resetChats', { commit })
+    },
+    clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
+      commit('clearOpenedChats', { commit })
+    }
+  },
+  mutations: {
+    setChatListFetcher (state, { commit, fetcher }) {
+      const prevFetcher = state.chatListFetcher
+      if (prevFetcher) {
+        clearInterval(prevFetcher)
+      }
+      state.chatListFetcher = fetcher && fetcher()
+    },
+    setCurrentChatFetcher (state, { fetcher }) {
+      const prevFetcher = state.fetcher
+      if (prevFetcher) {
+        clearInterval(prevFetcher)
+      }
+      state.fetcher = fetcher && fetcher()
+    },
+    addOpenedChat (state, { _dispatch, chat }) {
+      state.currentChatId = chat.id
+      Vue.set(state.openedChats, chat.id, chat)
+
+      if (!state.openedChatMessageServices[chat.id]) {
+        Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
+      }
+    },
+    setCurrentChatId (state, { chatId }) {
+      state.currentChatId = chatId
+    },
+    addNewChats (state, { _dispatch, chats, _rootGetters }) {
+      chats.forEach((updatedChat) => {
+        const chat = getChatById(state, updatedChat.id)
+
+        if (chat) {
+          chat.lastMessage = updatedChat.lastMessage
+          chat.unread = updatedChat.unread
+        } else {
+          state.chatList.data.push(updatedChat)
+          Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+        }
+      })
+    },
+    updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
+      const chat = getChatById(state, updatedChat.id)
+      if (chat) {
+        chat.lastMessage = updatedChat.lastMessage
+        chat.unread = updatedChat.unread
+        chat.updated_at = updatedChat.updated_at
+      }
+      if (!chat) { state.chatList.data.unshift(updatedChat) }
+      Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+    },
+    deleteChat (state, { _dispatch, id, _rootGetters }) {
+      state.chats.data = state.chats.data.filter(conversation =>
+        conversation.last_status.id !== id
+      )
+      state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
+    },
+    resetChats (state, { commit }) {
+      state.chatList = emptyChatList()
+      state.currentChatId = null
+      commit('setChatListFetcher', { fetcher: undefined })
+      for (const chatId in state.openedChats) {
+        chatService.clear(state.openedChatMessageServices[chatId])
+        Vue.delete(state.openedChats, chatId)
+        Vue.delete(state.openedChatMessageServices, chatId)
+      }
+    },
+    setChatsLoading (state, { value }) {
+      state.chats.loading = value
+    },
+    addChatMessages (state, { commit, chatId, messages }) {
+      const chatMessageService = state.openedChatMessageServices[chatId]
+      if (chatMessageService) {
+        chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
+        commit('refreshLastMessage', { chatId })
+      }
+    },
+    refreshLastMessage (state, { chatId }) {
+      const chatMessageService = state.openedChatMessageServices[chatId]
+      if (chatMessageService) {
+        const chat = getChatById(state, chatId)
+        if (chat) {
+          chat.lastMessage = chatMessageService.lastMessage
+          if (chatMessageService.lastMessage) {
+            chat.updated_at = chatMessageService.lastMessage.created_at
+          }
+        }
+      }
+    },
+    deleteChatMessage (state, { commit, chatId, messageId }) {
+      const chatMessageService = state.openedChatMessageServices[chatId]
+      if (chatMessageService) {
+        chatService.deleteMessage(chatMessageService, messageId)
+        commit('refreshLastMessage', { chatId })
+      }
+    },
+    resetChatNewMessageCount (state, _value) {
+      const chatMessageService = state.openedChatMessageServices[state.currentChatId]
+      chatService.resetNewMessageCount(chatMessageService)
+    },
+    // Used when a connection loss occurs
+    clearOpenedChats (state) {
+      const currentChatId = state.currentChatId
+      for (const chatId in state.openedChats) {
+        if (currentChatId !== chatId) {
+          chatService.clear(state.openedChatMessageServices[chatId])
+          Vue.delete(state.openedChats, chatId)
+          Vue.delete(state.openedChatMessageServices, chatId)
+        }
+      }
+    },
+    readChat (state, { id }) {
+      const chat = getChatById(state, id)
+      if (chat) {
+        chat.unread = 0
+      }
+    }
+  }
+}
+
+export default chats
diff --git a/src/modules/config.js b/src/modules/config.js
index 47b24d77..e0fe72df 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -46,7 +46,8 @@ export const defaultState = {
     repeats: true,
     moves: true,
     emojiReactions: false,
-    followRequest: true
+    followRequest: true,
+    chatMention: true
   },
   webPushNotifications: false,
   muteWords: [],
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 45a8eeca..3fe3bbf3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -55,6 +55,7 @@ const defaultState = {
 
   // Feature-set, apparently, not everything here is reported...
   chatAvailable: false,
+  pleromaChatMessagesAvailable: false,
   gopherAvailable: false,
   mediaProxyAvailable: false,
   suggestionsEnabled: false,
diff --git a/src/modules/interface.js b/src/modules/interface.js
index e31630fc..ec08ac0a 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -15,7 +15,8 @@ const defaultState = {
     )
   },
   mobileLayout: false,
-  globalNotices: []
+  globalNotices: [],
+  layoutHeight: 0
 }
 
 const interfaceMod = {
@@ -65,6 +66,9 @@ const interfaceMod = {
     },
     removeGlobalNotice (state, notice) {
       state.globalNotices = state.globalNotices.filter(n => n !== notice)
+    },
+    setLayoutHeight (state, value) {
+      state.layoutHeight = value
     }
   },
   actions: {
@@ -110,6 +114,9 @@ const interfaceMod = {
     },
     removeGlobalNotice ({ commit }, notice) {
       commit('removeGlobalNotice', notice)
+    },
+    setLayoutHeight ({ commit }, value) {
+      commit('setLayoutHeight', value)
     }
   }
 }
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 7fbf685c..64f5b587 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -478,7 +478,7 @@ export const mutations = {
   },
   setDeleted (state, { status }) {
     const newStatus = state.allStatusesObject[status.id]
-    newStatus.deleted = true
+    if (newStatus) newStatus.deleted = true
   },
   setManyDeleted (state, condition) {
     Object.values(state.allStatusesObject).forEach(status => {
@@ -521,6 +521,9 @@ export const mutations = {
   dismissNotification (state, { id }) {
     state.notifications.data = state.notifications.data.filter(n => n.id !== id)
   },
+  dismissNotifications (state, { finder }) {
+    state.notifications.data = state.notifications.data.filter(n => finder)
+  },
   updateNotification (state, { id, updater }) {
     const notification = find(state.notifications.data, n => n.id === id)
     notification && updater(notification)
diff --git a/src/modules/users.js b/src/modules/users.js
index 7e136c61..16c1e566 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -498,6 +498,7 @@ const users = {
           store.dispatch('stopFetchingFollowRequests')
           store.commit('clearNotifications')
           store.commit('resetStatuses')
+          store.dispatch('resetChats')
         })
     },
     loginUser (store, accessToken) {
@@ -537,6 +538,9 @@ const users = {
 
                 // Start fetching notifications
                 store.dispatch('startFetchingNotifications')
+
+                // Start fetching chats
+                store.dispatch('startFetchingChats')
               }
 
               if (store.getters.mergedConfig.useStreamingApi) {
@@ -544,6 +548,7 @@ const users = {
                   console.error('Failed initializing MastoAPI Streaming socket', error)
                   startPolling()
                 }).then(() => {
+                  store.dispatch('fetchChats', { latest: true })
                   setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
                 })
               } else {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 14e63e4f..5428cc2a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
 import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
@@ -81,6 +81,11 @@ const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
 const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
 const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
 const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
+const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
+const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
+const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
+const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
 
 const oldfetch = window.fetch
 
@@ -117,13 +122,18 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
   }
   return fetch(url, options)
     .then((response) => {
-      return new Promise((resolve, reject) => response.json()
-        .then((json) => {
-          if (!response.ok) {
-            return reject(new StatusCodeError(response.status, json, { url, options }, response))
-          }
-          return resolve(json)
-        }))
+      return new Promise((resolve, reject) => {
+        response.json()
+          .then((json) => {
+            if (!response.ok) {
+              return reject(new StatusCodeError(response.status, json, { url, options }, response))
+            }
+            return resolve(json)
+          })
+          .catch((error) => {
+            return reject(new StatusCodeError(response.status, error.message, { url, options }, response))
+          })
+      })
     })
 }
 
@@ -1067,6 +1077,10 @@ const MASTODON_STREAMING_EVENTS = new Set([
   'filters_changed'
 ])
 
+const PLEROMA_STREAMING_EVENTS = new Set([
+  'pleroma:chat_update'
+])
+
 // A thin wrapper around WebSocket API that allows adding a pre-processor to it
 // Uses EventTarget and a CustomEvent to proxy events
 export const ProcessedWS = ({
@@ -1123,7 +1137,7 @@ export const handleMastoWS = (wsEvent) => {
   if (!data) return
   const parsedEvent = JSON.parse(data)
   const { event, payload } = parsedEvent
-  if (MASTODON_STREAMING_EVENTS.has(event)) {
+  if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) {
     // MastoBE and PleromaBE both send payload for delete as a PLAIN string
     if (event === 'delete') {
       return { event, id: payload }
@@ -1133,6 +1147,8 @@ export const handleMastoWS = (wsEvent) => {
       return { event, status: parseStatus(data) }
     } else if (event === 'notification') {
       return { event, notification: parseNotification(data) }
+    } else if (event === 'pleroma:chat_update') {
+      return { event, chatUpdate: parseChat(data) }
     }
   } else {
     console.warn('Unknown event', wsEvent)
@@ -1140,6 +1156,81 @@ export const handleMastoWS = (wsEvent) => {
   }
 }
 
+export const WSConnectionStatus = Object.freeze({
+  'JOINED': 1,
+  'CLOSED': 2,
+  'ERROR': 3
+})
+
+const chats = ({ credentials }) => {
+  return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) })
+    .then((data) => data.json())
+    .then((data) => {
+      return { chats: data.map(parseChat).filter(c => c) }
+    })
+}
+
+const getOrCreateChat = ({ accountId, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_CHAT_URL(accountId),
+    method: 'POST',
+    credentials
+  })
+}
+
+const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
+  let url = PLEROMA_CHAT_MESSAGES_URL(id)
+  const args = [
+    maxId && `max_id=${maxId}`,
+    sinceId && `since_id=${sinceId}`,
+    limit && `limit=${limit}`
+  ].filter(_ => _).join('&')
+
+  url = url + (args ? '?' + args : '')
+
+  return promisedRequest({
+    url,
+    method: 'GET',
+    credentials
+  })
+}
+
+const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
+  const payload = {
+    'content': content
+  }
+
+  if (mediaId) {
+    payload['media_id'] = mediaId
+  }
+
+  return promisedRequest({
+    url: PLEROMA_CHAT_MESSAGES_URL(id),
+    method: 'POST',
+    payload: payload,
+    credentials
+  })
+}
+
+const readChat = ({ id, lastReadId, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_CHAT_READ_URL(id),
+    method: 'POST',
+    payload: {
+      'last_read_id': lastReadId
+    },
+    credentials
+  })
+}
+
+const deleteChatMessage = ({ chatId, messageId, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
+    method: 'DELETE',
+    credentials
+  })
+}
+
 const apiService = {
   verifyCredentials,
   fetchTimeline,
@@ -1218,7 +1309,13 @@ const apiService = {
   fetchKnownDomains,
   fetchDomainMutes,
   muteDomain,
-  unmuteDomain
+  unmuteDomain,
+  chats,
+  getOrCreateChat,
+  chatMessages,
+  sendChatMessage,
+  readChat,
+  deleteChatMessage
 }
 
 export default apiService
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
new file mode 100644
index 00000000..763a7607
--- /dev/null
+++ b/src/services/chat_service/chat_service.js
@@ -0,0 +1,150 @@
+import _ from 'lodash'
+
+const empty = (chatId) => {
+  return {
+    idIndex: {},
+    messages: [],
+    newMessageCount: 0,
+    lastSeenTimestamp: 0,
+    chatId: chatId,
+    minId: undefined,
+    lastMessage: undefined
+  }
+}
+
+const clear = (storage) => {
+  storage.idIndex = {}
+  storage.messages.splice(0, storage.messages.length)
+  storage.newMessageCount = 0
+  storage.lastSeenTimestamp = 0
+  storage.minId = undefined
+  storage.lastMessage = undefined
+}
+
+const deleteMessage = (storage, messageId) => {
+  if (!storage) { return }
+  storage.messages = storage.messages.filter(m => m.id !== messageId)
+  delete storage.idIndex[messageId]
+
+  if (storage.lastMessage && (storage.lastMessage.id === messageId)) {
+    storage.lastMessage = _.maxBy(storage.messages, 'id')
+  }
+
+  if (storage.minId === messageId) {
+    storage.minId = _.minBy(storage.messages, 'id')
+  }
+}
+
+const add = (storage, { messages: newMessages }) => {
+  if (!storage) { return }
+  for (let i = 0; i < newMessages.length; i++) {
+    const message = newMessages[i]
+
+    // sanity check
+    if (message.chat_id !== storage.chatId) { return }
+
+    if (!storage.minId || message.id < storage.minId) {
+      storage.minId = message.id
+    }
+
+    if (!storage.lastMessage || message.id > storage.lastMessage.id) {
+      storage.lastMessage = message
+    }
+
+    if (!storage.idIndex[message.id]) {
+      if (storage.lastSeenTimestamp < message.created_at) {
+        storage.newMessageCount++
+      }
+      storage.messages.push(message)
+      storage.idIndex[message.id] = message
+    }
+  }
+}
+
+const resetNewMessageCount = (storage) => {
+  if (!storage) { return }
+  storage.newMessageCount = 0
+  storage.lastSeenTimestamp = new Date()
+}
+
+// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
+const getView = (storage) => {
+  if (!storage) { return [] }
+
+  const result = []
+  const messages = _.sortBy(storage.messages, ['id', 'desc'])
+  const firstMessages = messages[0]
+  let prev = messages[messages.length - 1]
+  let currentMessageChainId
+
+  if (firstMessages) {
+    const date = new Date(firstMessages.created_at)
+    date.setHours(0, 0, 0, 0)
+    result.push({
+      type: 'date',
+      date,
+      id: date.getTime().toString()
+    })
+  }
+
+  let afterDate = false
+
+  for (let i = 0; i < messages.length; i++) {
+    const message = messages[i]
+    const nextMessage = messages[i + 1]
+
+    const date = new Date(message.created_at)
+    date.setHours(0, 0, 0, 0)
+
+    // insert date separator and start a new message chain
+    if (prev && prev.date < date) {
+      result.push({
+        type: 'date',
+        date,
+        id: date.getTime().toString()
+      })
+
+      prev['isTail'] = true
+      currentMessageChainId = undefined
+      afterDate = true
+    }
+
+    const object = {
+      type: 'message',
+      data: message,
+      date,
+      id: message.id,
+      messageChainId: currentMessageChainId
+    }
+
+    // end a message chian
+    if ((nextMessage && nextMessage.account_id) !== message.account_id) {
+      object['isTail'] = true
+      currentMessageChainId = undefined
+    }
+
+    // start a new message chain
+    if ((prev && prev.data && prev.data.account_id) !== message.account_id || afterDate) {
+      currentMessageChainId = _.uniqueId()
+      object['isHead'] = true
+      object['messageChainId'] = currentMessageChainId
+    }
+
+    result.push(object)
+    prev = object
+    afterDate = false
+  }
+
+  return result
+}
+
+const ChatService = {
+  add,
+  empty,
+  getView,
+  deleteMessage,
+  resetNewMessageCount,
+  clear
+}
+
+export default ChatService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ec83c02a..7ea8a16c 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -183,6 +183,7 @@ export const parseUser = (data) => {
     output.deactivated = data.pleroma.deactivated
 
     output.notification_settings = data.pleroma.notification_settings
+    output.unread_chat_count = data.pleroma.unread_chat_count
   }
 
   output.tags = output.tags || []
@@ -372,7 +373,7 @@ export const parseNotification = (data) => {
       ? parseStatus(data.notice.favorited_status)
       : parsedNotice
     output.action = parsedNotice
-    output.from_profile = parseUser(data.from_profile)
+    output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
   }
 
   output.created_at = new Date(data.created_at)
@@ -398,3 +399,34 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
     minId: flakeId ? minId : parseInt(minId, 10)
   }
 }
+
+export const parseChat = (chat) => {
+  const output = {}
+  output.id = chat.id
+  output.account = parseUser(chat.account)
+  output.unread = chat.unread
+  output.lastMessage = parseChatMessage(chat.last_message)
+  output.updated_at = new Date(chat.updated_at)
+  return output
+}
+
+export const parseChatMessage = (message) => {
+  if (!message) { return }
+  if (message.isNormalized) { return message }
+  const output = message
+  output.id = message.id
+  output.created_at = new Date(message.created_at)
+  output.chat_id = message.chat_id
+  if (message.content) {
+    output.content = addEmojis(message.content, message.emojis)
+  } else {
+    output.content = ''
+  }
+  if (message.attachment) {
+    output.attachments = [parseAttachment(message.attachment)]
+  } else {
+    output.attachments = []
+  }
+  output.isNormalized = true
+  return output
+}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index fbdcf562..07425abd 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -106,7 +106,8 @@ export const generateRadii = (input) => {
     avatar: 5,
     avatarAlt: 50,
     tooltip: 2,
-    attachment: 5
+    attachment: 5,
+    chatMessage: inputRadii.panel
   })
 
   return {
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 6b25cd6f..b58ca9be 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -23,7 +23,9 @@ export const LAYERS = {
   inputTopBar: 'topBar',
   alert: 'bg',
   alertPanel: 'panel',
-  poll: 'bg'
+  poll: 'bg',
+  chatBg: 'underlay',
+  chatMessage: 'chatBg'
 }
 
 /* By default opacity slots have 1 as default opacity
@@ -667,5 +669,54 @@ export const SLOT_INHERITANCE = {
     layer: 'badge',
     variant: 'badgeNotification',
     textColor: 'bw'
+  },
+
+  chatBg: {
+    depends: ['bg']
+  },
+
+  chatMessage: {
+    depends: ['chatBg']
+  },
+
+  chatMessageIncomingBg: {
+    depends: ['chatMessage'],
+    layer: 'chatMessage'
+  },
+
+  chatMessageIncomingText: {
+    depends: ['text'],
+    layer: 'text'
+  },
+
+  chatMessageIncomingLink: {
+    depends: ['link'],
+    layer: 'link'
+  },
+
+  chatMessageIncomingBorder: {
+    depends: ['border'],
+    opacity: 'border',
+    color: (mod, border) => brightness(2 * mod, border).rgb
+  },
+
+  chatMessageOutgoingBg: {
+    depends: ['chatMessage'],
+    color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
+  },
+
+  chatMessageOutgoingText: {
+    depends: ['text'],
+    layer: 'text'
+  },
+
+  chatMessageOutgoingLink: {
+    depends: ['link'],
+    layer: 'link'
+  },
+
+  chatMessageOutgoingBorder: {
+    depends: ['chatMessage'],
+    opacity: 'chatMessage'
   }
 }
diff --git a/src/services/window_utils/window_utils.js b/src/services/window_utils/window_utils.js
index faff6cb9..909088db 100644
--- a/src/services/window_utils/window_utils.js
+++ b/src/services/window_utils/window_utils.js
@@ -3,3 +3,8 @@ export const windowWidth = () =>
   window.innerWidth ||
   document.documentElement.clientWidth ||
   document.body.clientWidth
+
+export const windowHeight = () =>
+  window.innerHeight ||
+  document.documentElement.clientHeight ||
+  document.body.clientHeight
diff --git a/static/fontello.json b/static/fontello.json
index 5ef8544e..706800cd 100644
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -399,6 +399,12 @@
       "css": "doc",
       "code": 59433,
       "src": "fontawesome"
+    },
+    {
+      "uid": "98d9c83c1ee7c2c25af784b518c522c5",
+      "css": "block",
+      "code": 59434,
+      "src": "fontawesome"
     }
   ]
 }
\ No newline at end of file
diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js
index a415aeaf..3673256f 100644
--- a/test/unit/specs/boot/routes.spec.js
+++ b/test/unit/specs/boot/routes.spec.js
@@ -1,14 +1,22 @@
+import Vuex from 'vuex'
 import routes from 'src/boot/routes'
 import { createLocalVue } from '@vue/test-utils'
 import VueRouter from 'vue-router'
 
 const localVue = createLocalVue()
+localVue.use(Vuex)
 localVue.use(VueRouter)
 
+const store = new Vuex.Store({
+  state: {
+    instance: {}
+  }
+})
+
 describe('routes', () => {
   const router = new VueRouter({
     mode: 'abstract',
-    routes: routes({})
+    routes: routes(store)
   })
 
   it('root path', () => {
-- 
cgit v1.2.3-70-g09d2


From 3b2dfcaf5c402743a44eb315c519f1100d3f1006 Mon Sep 17 00:00:00 2001
From: eugenijm <eugenijm@protonmail.com>
Date: Tue, 7 Jul 2020 23:33:08 +0300
Subject: Add the single-line prop to StatusContent and use it for chat list
 items

---
 src/components/chat/chat.scss                     |  1 -
 src/components/chat_list_item/chat_list_item.scss | 12 ++++++++++--
 src/components/chat_list_item/chat_list_item.vue  |  7 +++++--
 src/components/chat_title/chat_title.vue          |  2 ++
 src/components/status_content/status_content.js   |  3 ++-
 src/components/status_content/status_content.vue  |  7 +++++++
 6 files changed, 26 insertions(+), 6 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 6ae7ebc9..012a1b1d 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -53,7 +53,6 @@
     display: flex;
     z-index: 2;
     position: sticky;
-    display: flex;
     overflow: hidden;
   }
 
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
index 3ec59ea2..617054ec 100644
--- a/src/components/chat_list_item/chat_list_item.scss
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -43,6 +43,7 @@
     white-space: nowrap;
     overflow: hidden;
     flex-shrink: 1;
+    line-height: 1.4em;
   }
 
   .chat-preview {
@@ -51,10 +52,9 @@
     white-space: nowrap;
     text-overflow: ellipsis;
     margin: 0.35em 0;
-    height: 1.2em;
-    line-height: 1.2em;
     color: $fallback--text;
     color: var(--faint, $fallback--text);
+    width: 100%;
   }
 
   a {
@@ -83,4 +83,12 @@
       height: 1.4em;
     }
   }
+
+  .time-wrapper {
+    line-height: 1.4em;
+  }
+
+  .single-line {
+    padding-right: 1em;
+  }
 }
diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue
index 640426b8..1f8ecdf6 100644
--- a/src/components/chat_list_item/chat_list_item.vue
+++ b/src/components/chat_list_item/chat_list_item.vue
@@ -23,7 +23,10 @@
         <span class="heading-right" />
       </div>
       <div class="chat-preview">
-        <StatusContent :status="messageForStatusContent" />
+        <StatusContent
+          :status="messageForStatusContent"
+          :single-line="true"
+        />
         <div
           v-if="chat.unread > 0"
           class="badge badge-notification unread-chat-count"
@@ -32,7 +35,7 @@
         </div>
       </div>
     </div>
-    <div>
+    <div class="time-wrapper">
       <Timeago
         :time="chat.updated_at"
         :auto-update="60"
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index cfd1e6d1..c375b10b 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -40,6 +40,8 @@
     white-space: nowrap;
     display: inline;
     word-wrap: break-word;
+    overflow: hidden;
+    text-overflow: ellipsis;
 
     .emoji {
       width: 14px;
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index dc83d8cb..df095de3 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -14,7 +14,8 @@ const StatusContent = {
     'status',
     'focused',
     'noHeading',
-    'fullContent'
+    'fullContent',
+    'singleLine'
   ],
   data () {
     return {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 9ca2cf6d..bf8d376e 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -43,6 +43,7 @@
       </a>
       <div
         v-if="!hideSubjectStatus"
+        :class="{ 'single-line': singleLine }"
         class="status-content media-body"
         @click.prevent="linkClicked"
         v-html="postBodyHtml"
@@ -269,6 +270,12 @@ $status-margin: 0.75em;
     h4 {
       margin: 1.1em 0;
     }
+
+    &.single-line {
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
   }
 }
 
-- 
cgit v1.2.3-70-g09d2


From 222ad2dd50823074ad323a4a9ff75842808fbe25 Mon Sep 17 00:00:00 2001
From: eugenijm <eugenijm@protonmail.com>
Date: Thu, 23 Jul 2020 16:39:01 +0300
Subject: Make the single line mode is consistent with status-content line
 height

---
 src/components/status_content/status_content.vue | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index bf8d376e..ab48786c 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -275,6 +275,7 @@ $status-margin: 0.75em;
       white-space: nowrap;
       text-overflow: ellipsis;
       overflow: hidden;
+      height: 1.4em;
     }
   }
 }
-- 
cgit v1.2.3-70-g09d2


From 65b6e23c00f09f3627e29987302cd16c9e069da2 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 28 Jul 2020 01:27:11 +0300
Subject: refactor status

---
 package.json                                       |    7 +-
 src/components/chat_list_item/chat_list_item.scss  |    2 +-
 src/components/conversation/conversation.vue       |   33 +-
 src/components/notifications/notifications.scss    |   15 +-
 src/components/status/status.scss                  |  563 ++++----
 src/components/status/status.vue                   |   48 +-
 src/components/status_content/status_content.vue   |   13 +-
 src/components/status_popover/status_popover.vue   |    3 +-
 src/components/timeline/timeline.vue               |    2 +-
 .../user_reporting_modal/user_reporting_modal.vue  |    3 +-
 yarn.lock                                          | 1437 +++++++++++++++++++-
 11 files changed, 1748 insertions(+), 378 deletions(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/package.json b/package.json
index 96231171..e289729c 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,8 @@
     "cropperjs": "^1.4.3",
     "diff": "^3.0.1",
     "escape-html": "^1.0.3",
-    "parse-link-header": "^1.0.1",
     "localforage": "^1.5.0",
+    "parse-link-header": "^1.0.1",
     "phoenix": "^1.3.0",
     "portal-vue": "^2.1.4",
     "v-click-outside": "^2.1.1",
@@ -36,7 +36,6 @@
     "vuex": "^3.0.1"
   },
   "devDependencies": {
-    "karma-mocha-reporter": "^2.2.1",
     "@babel/core": "^7.7.5",
     "@babel/plugin-transform-runtime": "^7.7.6",
     "@babel/preset-env": "^7.7.6",
@@ -80,6 +79,7 @@
     "karma-coverage": "^1.1.1",
     "karma-firefox-launcher": "^1.1.0",
     "karma-mocha": "^1.2.0",
+    "karma-mocha-reporter": "^2.2.1",
     "karma-sinon-chai": "^2.0.2",
     "karma-sourcemap-loader": "^0.3.7",
     "karma-spec-reporter": "0.0.26",
@@ -101,6 +101,9 @@
     "shelljs": "^0.7.4",
     "sinon": "^2.1.0",
     "sinon-chai": "^2.8.0",
+    "stylelint": "^13.6.1",
+    "stylelint-config-standard": "^20.0.0",
+    "stylelint-rscss": "^0.4.0",
     "url-loader": "^1.1.2",
     "vue-loader": "^14.0.0",
     "vue-style-loader": "^4.0.0",
diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss
index 617054ec..10ba45f2 100644
--- a/src/components/chat_list_item/chat_list_item.scss
+++ b/src/components/chat_list_item/chat_list_item.scss
@@ -77,7 +77,7 @@
     border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
   }
 
-  .status-body {
+  .StatusContent {
     img.emoji {
       width: 1.4em;
       height: 1.4em;
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 2e48240a..7e2c6a9d 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,7 +1,7 @@
 <template>
   <div
-    class="timeline panel-default"
-    :class="[isExpanded ? 'panel' : 'panel-disabled']"
+    class="Conversation"
+    :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
   >
     <div
       v-if="isExpanded"
@@ -28,7 +28,7 @@
       :replies="getReplies(status.id)"
       :in-profile="inProfile"
       :profile-user-id="profileUserId"
-      class="status-fadein panel-body"
+      class="conversation-status status-fadein panel-body"
       @goto="setHighlight"
       @toggleExpanded="toggleExpanded"
     />
@@ -40,14 +40,25 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
-.timeline {
-  .panel-disabled {
-    .status-el {
-      border-left: none;
-      border-bottom-width: 1px;
-      border-bottom-style: solid;
-      border-color: var(--border, $fallback--border);
-      border-radius: 0;
+.Conversation {
+  .conversation-status {
+    border-left: none;
+    border-bottom-width: 1px;
+    border-bottom-style: solid;
+    border-color: var(--border, $fallback--border);
+    border-radius: 0;
+  }
+
+  &.-expanded {
+    .conversation-status:last-child {
+      border-bottom: none;
+    }
+  }
+
+  &:not(.-expanded) > {
+    .conversation-status:last-child {
+      border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+      border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
     }
   }
 }
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index b1a3ad70..1bd91995 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -60,16 +60,8 @@
       height: 32px;
     }
 
-    .status-body {
-      color: $fallback--faint;
-      color: var(--faint, $fallback--faint);
-      a {
-        color: var(--faintLink);
-      }
-      .status-content a {
-        color: var(--postFaintLink);
-      }
-    }
+    --link: var(--faintLink);
+    --text: var(--faint);
   }
 
   .follow-request-accept {
@@ -106,7 +98,8 @@
     }
   }
 
-  .status-el {
+  /* TODO cleanup this */
+  .Status {
     flex: 1;
   }
 
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 05edd1bf..85e433c4 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -3,36 +3,31 @@
 
 $status-margin: 0.75em;
 
-.status-body {
-  flex: 1;
-  min-width: 0;
-}
-
-.status-pin {
-  padding: $status-margin $status-margin 0;
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-}
-
-.media-left {
-  margin-right: $status-margin;
-}
-
-.status-el {
-  border-left-width: 0px;
+.Status {
   min-width: 0;
   border-color: $fallback--border;
   border-color: var(--border, $fallback--border);
-
   border-left: 4px $fallback--cRed;
   border-left: 4px var(--cRed, $fallback--cRed);
 
-  &_focused {
+  // TODO find a way to do this in a cleaner way without JS
+  // stylelint-disable rscss/class-format
+  &:hover .avatar.animated {
+    canvas {
+      display: none;
+    }
+
+    img {
+      visibility: visible;
+    }
+  }
+
+  &.-focused {
     background-color: $fallback--lightBg;
     background-color: var(--selectedPost, $fallback--lightBg);
     color: $fallback--text;
     color: var(--selectedPostText, $fallback--text);
+
     --lightText: var(--selectedPostLightText, $fallback--light);
     --faint: var(--selectedPostFaintText, $fallback--faint);
     --faintLink: var(--selectedPostFaintLink, $fallback--faint);
@@ -41,17 +36,32 @@ $status-margin: 0.75em;
     --icon: var(--selectedPostIcon, $fallback--icon);
   }
 
-  .timeline & {
-    border-bottom-width: 1px;
-    border-bottom-style: solid;
+  .status-container {
+    display: flex;
+    padding: $status-margin;
+
+    &.-repeat {
+      padding-top: 0;
+    }
   }
 
-  .media-body {
+  .pin {
+    padding: $status-margin $status-margin 0;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+  }
+
+  .left-side {
+    margin-right: $status-margin;
+  }
+
+  .right-side {
     flex: 1;
-    padding: 0;
+    min-width: 0;
   }
 
-  .status-usercard {
+  .usercard {
     margin-bottom: $status-margin;
   }
 
@@ -59,15 +69,17 @@ $status-margin: 0.75em;
     white-space: nowrap;
     font-size: 14px;
     overflow: hidden;
-    flex-shrink: 0;
     max-width: 85%;
     font-weight: bold;
+    flex-shrink: 1;
+    margin-right: 0.4em;
+    text-overflow: ellipsis;
 
-    img.emoji {
+    .emoji {
       width: 14px;
       height: 14px;
       vertical-align: middle;
-      object-fit: contain
+      object-fit: contain;
     }
   }
 
@@ -77,358 +89,305 @@ $status-margin: 0.75em;
     margin-right: 0.4em;
   }
 
-  .media-heading {
-    padding: 0;
-    vertical-align: bottom;
-    flex-basis: 100%;
+  .status-heading {
     margin-bottom: 0.5em;
+  }
 
-    small {
-      font-weight: lighter;
-    }
+  .heading-name-row {
+    display: flex;
+    justify-content: space-between;
+    line-height: 18px;
 
-    .heading-name-row {
-      padding: 0;
-      display: flex;
-      justify-content: space-between;
-      line-height: 18px;
+    a {
+      display: inline-block;
+      word-break: break-all;
+    }
+  }
 
-      a {
-        display: inline-block;
-        word-break: break-all;
-      }
+  .account-name {
+    min-width: 1.6em;
+    margin-right: 0.4em;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    flex: 1 1 0;
+  }
 
-      .name-and-account-name {
-        display: flex;
-        min-width: 0;
-      }
+  .heading-left {
+    display: flex;
+    min-width: 0;
+  }
 
-      .user-name {
-        flex-shrink: 1;
-        margin-right: 0.4em;
-        overflow: hidden;
-        text-overflow: ellipsis;
-      }
+  .heading-right {
+    display: flex;
+    flex-shrink: 0;
+  }
 
-      .account-name {
-        min-width: 1.6em;
-        margin-right: 0.4em;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        flex: 1 1 0;
-      }
-    }
+  .timeago {
+    margin-right: 0.2em;
+  }
 
-    .heading-right {
-      display: flex;
-      flex-shrink: 0;
-    }
+  .heading-reply-row {
+    position: relative;
+    align-content: baseline;
+    font-size: 12px;
+    line-height: 18px;
+    max-width: 100%;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: stretch;
+  }
 
-    .timeago {
-      margin-right: 0.2em;
-    }
+  .reply-to-and-accountname {
+    display: flex;
+    height: 18px;
+    margin-right: 0.5em;
+    max-width: 100%;
 
-    .heading-reply-row {
-      position: relative;
-      align-content: baseline;
-      font-size: 12px;
-      line-height: 18px;
-      max-width: 100%;
-      display: flex;
-      flex-wrap: wrap;
-      align-items: stretch;
-
-      > .reply-to-and-accountname > a {
-        overflow: hidden;
-        max-width: 100%;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-        word-break: break-all;
-      }
+    .icon-reply {
+      // mirror the icon
+      transform: scaleX(-1);
     }
+  }
 
-    .reply-to-and-accountname {
-      display: flex;
-      height: 18px;
-      margin-right: 0.5em;
-      max-width: 100%;
-      .icon-reply {
-        transform: scaleX(-1);
-      }
-    }
+  .reply-to-popover {
+    min-width: 0;
+    margin-right: 0.4em;
 
-    .reply-info {
-      display: flex;
+    &:hover {
+      border-bottom: 1px solid var(--faint);
     }
 
-    .reply-to-popover {
-      min-width: 0;
+    .faint-link:hover {
+      // override default
+      text-decoration: none;
     }
+  }
 
-    .reply-to {
-      display: flex;
-    }
+  .reply-to {
+    display: flex;
+  }
 
-    .reply-to-text {
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      margin: 0 0.4em 0 0.2em;
-    }
+  .reply-to-text {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    margin-left: 0.2em;
 
-    .strikethrough {
+    &.-strikethrough {
       text-decoration: line-through;
     }
+  }
 
-    .replies-separator {
-      margin-left: 0.4em;
-    }
+  .replies-separator {
+    margin-left: 0.4em;
+  }
 
-    .replies {
-      line-height: 18px;
-      font-size: 12px;
-      display: flex;
-      flex-wrap: wrap;
-      & > * {
-        margin-right: 0.4em;
-      }
-    }
+  .replies {
+    line-height: 18px;
+    font-size: 12px;
+    display: flex;
+    flex-wrap: wrap;
 
-    .reply-link {
-      height: 17px;
+    & > * {
+      margin-right: 0.4em;
     }
   }
 
-  .retweet-info {
-    padding: 0.4em $status-margin;
-    margin: 0;
-
-    .avatar.still-image {
-      border-radius: $fallback--avatarAltRadius;
-      border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
-      margin-left: 28px;
-      width: 20px;
-      height: 20px;
-    }
+  .reply-link {
+    height: 17px;
+  }
 
-    .media-body {
-      font-size: 1em;
-      line-height: 22px;
+  .repeat-info {
+    padding: 0.4em $status-margin;
+    line-height: 22px;
 
+    .right-side {
       display: flex;
       align-content: center;
       flex-wrap: wrap;
+    }
 
-      .user-name {
-        font-weight: bold;
-        overflow: hidden;
-        text-overflow: ellipsis;
-
-        img {
-          width: 14px;
-          height: 14px;
-          vertical-align: middle;
-          object-fit: contain
-        }
-      }
-
-      i {
-        padding: 0 0.2em;
-      }
-
-      a {
-        max-width: 100%;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-      }
+    i {
+      padding: 0 0.2em;
     }
   }
-}
-
-.status-fadein {
-  animation-duration: 0.4s;
-  animation-name: fadein;
-}
 
-@keyframes fadein {
-  from {
-    opacity: 0;
+  .repeater-avatar {
+    border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+    margin-left: 28px;
+    width: 20px;
+    height: 20px;
   }
-  to {
-    opacity: 1;
-  }
-}
-
-.status-conversation {
-  border-left-style: solid;
-}
 
-.status-actions {
-  position: relative;
-  width: 100%;
-  display: flex;
-  margin-top: $status-margin;
+  .repeater-name {
+    text-overflow: ellipsis;
+    margin-right: 0;
 
-  > * {
-    max-width: 4em;
-    flex: 1;
+    .emoji {
+      width: 14px;
+      height: 14px;
+      vertical-align: middle;
+      object-fit: contain;
+    }
   }
-}
 
-.button-icon.icon-reply {
-  &:not(.button-icon-disabled):hover,
-  &.button-icon-active {
-    color: $fallback--cBlue;
-    color: var(--cBlue, $fallback--cBlue);
+  .status-fadein {
+    animation-duration: 0.4s;
+    animation-name: fadein;
   }
-}
 
-.button-icon.icon-reply {
-  &:not(.button-icon-disabled) {
-    cursor: pointer;
-  }
-}
+  @keyframes fadein {
+    from {
+      opacity: 0;
+    }
 
-.status:hover .animated.avatar {
-  canvas {
-    display: none;
-  }
-  img {
-    visibility: visible;
+    to {
+      opacity: 1;
+    }
   }
-}
 
-.status {
-  display: flex;
-  padding: $status-margin;
-  &.is-retweet {
-    padding-top: 0;
+  .status-actions {
+    position: relative;
+    width: 100%;
+    display: flex;
+    margin-top: $status-margin;
+
+    > * {
+      max-width: 4em;
+      flex: 1;
+    }
   }
-}
 
-.status-conversation:last-child {
-  border-bottom: none;
-}
+  .button-reply {
+    &:not(.-disabled) {
+      cursor: pointer;
+    }
 
-.muted {
-  padding: .25em .6em;
-  height: 1.2em;
-  line-height: 1.2em;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  display: flex;
-  flex-wrap: nowrap;
-
-  .username, .mute-thread, .mute-words {
-    word-wrap: normal;
-    word-break: normal;
-    white-space: nowrap;
+    &:not(.-disabled):hover,
+    &.-active {
+      color: $fallback--cBlue;
+      color: var(--cBlue, $fallback--cBlue);
+    }
   }
 
-  .username, .mute-words {
+  .muted {
+    padding: 0.25em 0.6em;
+    height: 1.2em;
+    line-height: 1.2em;
     text-overflow: ellipsis;
     overflow: hidden;
-  }
+    display: flex;
+    flex-wrap: nowrap;
 
-  .username {
-    flex: 0 1 auto;
-    margin-right: .2em;
-  }
+    & .user-name,
+    & .mute-thread,
+    & .mute-words {
+      word-wrap: normal;
+      word-break: normal;
+      white-space: nowrap;
+    }
 
-  .mute-thread {
-    flex: 0 0 auto;
-  }
+    & .user-name,
+    & .mute-words {
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
 
-  .mute-words {
-    flex: 1 0 5em;
-    margin-left: .2em;
-    &::before {
-      content: ' '
+    .user-name {
+      font-weight: normal;
+      flex: 0 1 auto;
+      margin-right: 0.2em;
+    }
+
+    .mute-thread {
+      flex: 0 0 auto;
     }
-  }
 
-  .unmute {
-    flex: 0 0 auto;
-    margin-left: auto;
-    display: block;
-    margin-left: auto;
+    .mute-words {
+      flex: 1 0 5em;
+      margin-left: 0.2em;
+
+      &::before {
+        content: ' ';
+      }
+    }
+
+    .unmute {
+      flex: 0 0 auto;
+      margin-left: auto;
+      display: block;
+    }
   }
-}
 
-.reply-body {
-  flex: 1;
-}
+  .reply-body {
+    flex: 1;
+  }
 
-.favs-repeated-users {
-  margin-top: $status-margin;
+  .favs-repeated-users {
+    margin-top: $status-margin;
+  }
 
   .stats {
     width: 100%;
     display: flex;
     line-height: 1em;
+  }
 
-    .stat-count {
-      margin-right: $status-margin;
-      user-select: none;
+  .avatar-row {
+    flex: 1;
+    overflow: hidden;
+    position: relative;
+    display: flex;
+    align-items: center;
 
-      &:hover .stat-title {
-        text-decoration: underline;
-      }
+    &::before {
+      content: '';
+      position: absolute;
+      height: 100%;
+      width: 1px;
+      left: 0;
+      background-color: var(--faint, $fallback--faint);
+    }
+  }
 
-      .stat-title {
-        color: var(--faint, $fallback--faint);
-        font-size: 12px;
-        text-transform: uppercase;
-        position: relative;
-      }
+  .stat-count {
+    margin-right: $status-margin;
+    user-select: none;
 
-      .stat-number {
-        font-weight: bolder;
-        font-size: 16px;
-        line-height: 1em;
-      }
+    .stat-title {
+      color: var(--faint, $fallback--faint);
+      font-size: 12px;
+      text-transform: uppercase;
+      position: relative;
     }
 
-    .avatar-row {
-      flex: 1;
-      overflow: hidden;
-      position: relative;
-      display: flex;
-      align-items: center;
+    .stat-number {
+      font-weight: bolder;
+      font-size: 16px;
+      line-height: 1em;
+    }
 
-      &::before {
-        content: '';
-        position: absolute;
-        height: 100%;
-        width: 1px;
-        left: 0;
-        background-color: var(--faint, $fallback--faint);
-      }
+    &:hover .stat-title {
+      text-decoration: underline;
     }
   }
-}
 
-@media all and (max-width: 800px) {
-  .status-el {
-    .retweet-info {
-      .avatar.still-image {
-        margin-left: 20px;
-      }
+  @media all and (max-width: 800px) {
+    .repeater-avatar {
+      margin-left: 20px;
     }
-  }
-  .status {
-    max-width: 100%;
-  }
 
-  .status .avatar.still-image {
-    width: 40px;
-    height: 40px;
+    .avatar:not(.repeater-avatar) {
+      width: 40px;
+      height: 40px;
 
-    &.avatar-compact {
-      width: 32px;
-      height: 32px;
+      // TODO define those other way somehow?
+      // stylelint-disable rscss/class-format
+      &.avatar-compact {
+        width: 32px;
+        height: 32px;
+      }
     }
   }
 }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index b1a53df6..2b18b2f6 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -2,8 +2,8 @@
   <!-- eslint-disable vue/no-v-html -->
   <div
     v-if="!hideStatus"
-    class="status-el"
-    :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"
+    class="Status"
+    :class="[{ '-focused': isFocused }, { '-conversation': inlineExpanded }]"
   >
     <div
       v-if="error"
@@ -16,8 +16,8 @@
       />
     </div>
     <template v-if="muted && !isPreview">
-      <div class="media status container muted">
-        <small class="username">
+      <div class="status-csontainer muted">
+        <small class="user-name">
           <i
             v-if="muted && retweet"
             class="button-icon icon-retweet"
@@ -54,7 +54,7 @@
     <template v-else>
       <div
         v-if="showPinned"
-        class="status-pin"
+        class="pin"
       >
         <i class="fa icon-pin faint" />
         <span class="faint">{{ $t('status.pinned') }}</span>
@@ -63,17 +63,17 @@
         v-if="retweet && !noHeading && !inConversation"
         :class="[repeaterClass, { highlighted: repeaterStyle }]"
         :style="[repeaterStyle]"
-        class="media container retweet-info"
+        class="status-container repeat-info"
       >
         <UserAvatar
           v-if="retweet"
-          class="media-left"
+          class="left-side repeater-avatar"
           :better-shadow="betterShadow"
           :user="statusoid.user"
         />
-        <div class="media-body faint">
+        <div class="right-side faint">
           <span
-            class="user-name"
+            class="user-name repeater-name"
             :title="retweeter"
           >
             <router-link
@@ -95,14 +95,14 @@
       </div>
 
       <div
-        :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]"
+        :class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]"
         :style="[ userStyle ]"
-        class="media status"
+        class="status-container"
         :data-tags="tags"
       >
         <div
           v-if="!noHeading"
-          class="media-left"
+          class="left-side"
         >
           <router-link
             :to="userProfileLink"
@@ -115,20 +115,20 @@
             />
           </router-link>
         </div>
-        <div class="status-body">
+        <div class="right-side">
           <UserCard
             v-if="userExpanded"
             :user-id="status.user.id"
             :rounded="true"
             :bordered="true"
-            class="status-usercard"
+            class="usercard"
           />
           <div
             v-if="!noHeading"
-            class="media-heading"
+            class="status-heading"
           >
             <div class="heading-name-row">
-              <div class="name-and-account-name">
+              <div class="heading-left">
                 <h4
                   v-if="status.user.name_html"
                   class="user-name"
@@ -218,10 +218,10 @@
                     :aria-label="$t('tool_tip.reply')"
                     @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
                   >
-                    <i class="button-icon icon-reply" />
+                    <i class="button-icon reply-button icon-reply" />
                     <span
                       class="faint-link reply-to-text"
-                      :class="{ 'strikethrough': !status.parent_visible }"
+                      :class="{ '-strikethrough': !status.parent_visible }"
                     >
                       {{ $t('status.reply_to') }}
                     </span>
@@ -317,19 +317,19 @@
 
           <div
             v-if="!noHeading && !isPreview"
-            class="status-actions media-body"
+            class="status-actions"
           >
             <div>
               <i
                 v-if="loggedIn"
-                class="button-icon icon-reply"
+                class="button-icon button-reply icon-reply"
                 :title="$t('tool_tip.reply')"
-                :class="{'button-icon-active': replying}"
+                :class="{'-active': replying}"
                 @click.prevent="toggleReplying"
               />
               <i
                 v-else
-                class="button-icon button-icon-disabled icon-reply"
+                class="button-icon button-reply -disabled icon-reply"
                 :title="$t('tool_tip.reply')"
               />
               <span v-if="status.replies_count > 0">{{ status.replies_count }}</span>
@@ -357,7 +357,7 @@
       </div>
       <div
         v-if="replying"
-        class="container"
+        class="status-container"
       >
         <PostStatusForm
           class="reply-body"
@@ -375,4 +375,4 @@
 </template>
 
 <script src="./status.js" ></script>
-<style src="./status.css" lang="scss"></style>
+<style src="./status.scss" lang="scss"></style>
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index bf8d376e..8539933c 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,6 +1,6 @@
 <template>
   <!-- eslint-disable vue/no-v-html -->
-  <div class="status-body">
+  <div class="StatusContent">
     <slot name="header" />
     <div
       v-if="status.summary_html"
@@ -133,7 +133,7 @@
 
 $status-margin: 0.75em;
 
-.status-body {
+.StatusContent {
   flex: 1;
   min-width: 0;
 
@@ -283,13 +283,4 @@ $status-margin: 0.75em;
   color: $fallback--cGreen;
   color: var(--postGreentext, $fallback--cGreen);
 }
-
-.timeline :not(.panel-disabled) > {
-  .status-el:last-child {
-    border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
-    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
-    border-bottom: none;
-  }
-}
-
 </style>
diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue
index f5948207..40a7fa27 100644
--- a/src/components/status_popover/status_popover.vue
+++ b/src/components/status_popover/status_popover.vue
@@ -52,7 +52,8 @@
   box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
   box-shadow: var(--popupShadow);
 
-  .status-el.status-el {
+  /* TODO cleanup this */
+  .Status.Status {
     border: none;
   }
 
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 111c0976..4f4caf93 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,5 +1,5 @@
 <template>
-  <div :class="classes.root">
+  <div :class="[classes.root, 'timeline']">
     <div :class="classes.header">
       <div class="title">
         {{ title }}
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 6ee53461..2a8d8d48 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -146,7 +146,8 @@
     display: flex;
     justify-content: space-between;
 
-    > .status-el {
+    /* TODO cleanup this */
+    > .Status {
       flex: 1;
     }
 
diff --git a/yarn.lock b/yarn.lock
index 09316863..14be84a6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8,6 +8,13 @@
   dependencies:
     "@babel/highlight" "^7.0.0"
 
+"@babel/code-frame@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
+  integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
+  dependencies:
+    "@babel/highlight" "^7.10.4"
+
 "@babel/code-frame@^7.5.5":
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
@@ -15,6 +22,28 @@
   dependencies:
     "@babel/highlight" "^7.0.0"
 
+"@babel/core@>=7.9.0":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330"
+  integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.10.5"
+    "@babel/helper-module-transforms" "^7.10.5"
+    "@babel/helpers" "^7.10.4"
+    "@babel/parser" "^7.10.5"
+    "@babel/template" "^7.10.4"
+    "@babel/traverse" "^7.10.5"
+    "@babel/types" "^7.10.5"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.19"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
 "@babel/core@^7.7.5":
   version "7.7.5"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e"
@@ -35,6 +64,15 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69"
+  integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig==
+  dependencies:
+    "@babel/types" "^7.10.5"
+    jsesc "^2.5.1"
+    source-map "^0.5.0"
+
 "@babel/generator@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369"
@@ -94,6 +132,15 @@
     "@babel/traverse" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/helper-function-name@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
+  integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.10.4"
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
 "@babel/helper-function-name@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e"
@@ -103,6 +150,13 @@
     "@babel/template" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/helper-get-function-arity@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
+  integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
 "@babel/helper-get-function-arity@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0"
@@ -117,6 +171,13 @@
   dependencies:
     "@babel/types" "^7.7.4"
 
+"@babel/helper-member-expression-to-functions@^7.10.4":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee"
+  integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA==
+  dependencies:
+    "@babel/types" "^7.10.5"
+
 "@babel/helper-member-expression-to-functions@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz#356438e2569df7321a8326644d4b790d2122cb74"
@@ -137,6 +198,26 @@
   dependencies:
     "@babel/types" "^7.0.0"
 
+"@babel/helper-module-imports@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
+  integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-module-transforms@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6"
+  integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.10.4"
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.10.5"
+    lodash "^4.17.19"
+
 "@babel/helper-module-transforms@^7.7.4", "@babel/helper-module-transforms@^7.7.5":
   version "7.7.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.7.5.tgz#d044da7ffd91ec967db25cd6748f704b6b244835"
@@ -149,6 +230,13 @@
     "@babel/types" "^7.7.4"
     lodash "^4.17.13"
 
+"@babel/helper-optimise-call-expression@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
+  integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
 "@babel/helper-optimise-call-expression@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz#034af31370d2995242aa4df402c3b7794b2dcdf2"
@@ -179,6 +267,16 @@
     "@babel/traverse" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/helper-replace-supers@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf"
+  integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.10.4"
+    "@babel/helper-optimise-call-expression" "^7.10.4"
+    "@babel/traverse" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
 "@babel/helper-replace-supers@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz#3c881a6a6a7571275a72d82e6107126ec9e2cdd2"
@@ -189,6 +287,14 @@
     "@babel/traverse" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/helper-simple-access@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
+  integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==
+  dependencies:
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
 "@babel/helper-simple-access@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.7.4.tgz#a169a0adb1b5f418cfc19f22586b2ebf58a9a294"
@@ -197,6 +303,13 @@
     "@babel/template" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/helper-split-export-declaration@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1"
+  integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
 "@babel/helper-split-export-declaration@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz#57292af60443c4a3622cf74040ddc28e68336fd8"
@@ -204,6 +317,11 @@
   dependencies:
     "@babel/types" "^7.7.4"
 
+"@babel/helper-validator-identifier@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
+  integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+
 "@babel/helper-wrap-function@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.7.4.tgz#37ab7fed5150e22d9d7266e830072c0cdd8baace"
@@ -214,6 +332,15 @@
     "@babel/traverse" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/helpers@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044"
+  integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==
+  dependencies:
+    "@babel/template" "^7.10.4"
+    "@babel/traverse" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
 "@babel/helpers@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.4.tgz#62c215b9e6c712dadc15a9a0dcab76c92a940302"
@@ -231,6 +358,20 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
+"@babel/highlight@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
+  integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.4"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.10.4", "@babel/parser@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b"
+  integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ==
+
 "@babel/parser@^7.7.4", "@babel/parser@^7.7.5":
   version "7.7.5"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71"
@@ -663,6 +804,15 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
+"@babel/template@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
+  integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/parser" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
 "@babel/template@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b"
@@ -672,6 +822,21 @@
     "@babel/parser" "^7.7.4"
     "@babel/types" "^7.7.4"
 
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564"
+  integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.10.5"
+    "@babel/helper-function-name" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.10.4"
+    "@babel/parser" "^7.10.5"
+    "@babel/types" "^7.10.5"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.19"
+
 "@babel/traverse@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558"
@@ -695,6 +860,15 @@
     lodash "^4.17.10"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.10.4", "@babel/types@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15"
+  integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.4"
+    lodash "^4.17.19"
+    to-fast-properties "^2.0.0"
+
 "@babel/types@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193"
@@ -710,6 +884,67 @@
   dependencies:
     qrcode "^1.3.0"
 
+"@nodelib/fs.scandir@2.1.3":
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
+  integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.3"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
+  integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
+  integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.3"
+    fastq "^1.6.0"
+
+"@stylelint/postcss-css-in-js@^0.37.1":
+  version "0.37.2"
+  resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2"
+  integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==
+  dependencies:
+    "@babel/core" ">=7.9.0"
+
+"@stylelint/postcss-markdown@^0.36.1":
+  version "0.36.1"
+  resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.1.tgz#829b87e6c0f108014533d9d7b987dc9efb6632e8"
+  integrity sha512-iDxMBWk9nB2BPi1VFQ+Dc5+XpvODBHw2n3tYpaBZuEAFQlbtF9If0Qh5LTTwSi/XwdbJ2jt+0dis3i8omyggpw==
+  dependencies:
+    remark "^12.0.0"
+    unist-util-find-all-after "^3.0.1"
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+"@types/minimist@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
+  integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
+
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
+  integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+"@types/unist@^2.0.0", "@types/unist@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
+  integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
+
 "@ungap/event-target@^0.1.0":
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
@@ -940,6 +1175,16 @@ ajv@^6.1.0, ajv@^6.9.1:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ajv@^6.10.2:
+  version "6.12.3"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
+  integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
 ajv@^6.5.5:
   version "6.6.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.2.tgz#caceccf474bf3fc3ce3b147443711a24063cc30d"
@@ -981,6 +1226,11 @@ ansi-regex@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
 
+ansi-regex@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+  integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+
 ansi-styles@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@@ -991,6 +1241,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   dependencies:
     color-convert "^1.9.0"
 
+ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
 ansi-styles@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
@@ -1062,6 +1320,11 @@ array-union@^1.0.1:
   dependencies:
     array-uniq "^1.0.1"
 
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
 array-uniq@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@@ -1078,6 +1341,16 @@ arraybuffer.slice@~0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
 
+arrify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+  integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+
+arrify@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
+  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
+
 asn1.js@^4.0.0:
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@@ -1165,6 +1438,19 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0:
     postcss "^5.2.16"
     postcss-value-parser "^3.2.3"
 
+autoprefixer@^9.8.0:
+  version "9.8.5"
+  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa"
+  integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg==
+  dependencies:
+    browserslist "^4.12.0"
+    caniuse-lite "^1.0.30001097"
+    colorette "^1.2.0"
+    normalize-range "^0.1.2"
+    num2fraction "^1.2.2"
+    postcss "^7.0.32"
+    postcss-value-parser "^4.1.0"
+
 aws-sign2@~0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -1328,6 +1614,11 @@ backo2@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
 
+bail@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
+  integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
+
 balanced-match@^0.4.2:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -1478,6 +1769,13 @@ braces@^2.3.1, braces@^2.3.2:
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
+braces@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
 brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -1546,6 +1844,16 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
     caniuse-db "^1.0.30000639"
     electron-to-chromium "^1.2.7"
 
+browserslist@^4.12.0:
+  version "4.13.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d"
+  integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==
+  dependencies:
+    caniuse-lite "^1.0.30001093"
+    electron-to-chromium "^1.3.488"
+    escalade "^3.0.1"
+    node-releases "^1.1.58"
+
 browserslist@^4.6.0, browserslist@^4.8.2:
   version "4.8.2"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.2.tgz#b45720ad5fbc8713b7253c20766f701c9a694289"
@@ -1682,6 +1990,15 @@ camelcase-keys@^2.0.0:
     camelcase "^2.0.0"
     map-obj "^1.0.0"
 
+camelcase-keys@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
+  integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
+  dependencies:
+    camelcase "^5.3.1"
+    map-obj "^4.0.0"
+    quick-lru "^4.0.1"
+
 camelcase@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@@ -1690,6 +2007,16 @@ camelcase@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
 
+camelcase@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+camelcase@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
+  integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
+
 can-promise@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/can-promise/-/can-promise-0.0.1.tgz#7a7597ad801fb14c8b22341dfec314b6bd6ad8d3"
@@ -1714,10 +2041,20 @@ caniuse-lite@^1.0.30001015:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001015.tgz#15a7ddf66aba786a71d99626bc8f2b91c6f0f5f0"
   integrity sha512-/xL2AbW/XWHNu1gnIrO8UitBGoFthcsDgU9VLK1/dpsoxbaD5LscHozKze05R6WLsBvLhqv78dAPozMFQBYLbQ==
 
+caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097:
+  version "1.0.30001107"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001107.tgz#809360df7a5b3458f627aa46b0f6ed6d5239da9a"
+  integrity sha512-86rCH+G8onCmdN4VZzJet5uPELII59cUzDphko3thQFgAQG1RNa+sVLDoALIhRYmflo5iSIzWY3vu1XTWtNMQQ==
+
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
 
+ccount@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17"
+  integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==
+
 chai-nightwatch@~0.1.x:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9"
@@ -1758,6 +2095,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
+chalk@^4.0.0, chalk@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
+  integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
 chalk@~0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
@@ -1766,6 +2111,26 @@ chalk@~0.4.0:
     has-color "~0.1.0"
     strip-ansi "~0.1.0"
 
+character-entities-html4@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125"
+  integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==
+
+character-entities-legacy@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
+  integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
+
+character-entities@^1.0.0:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
+  integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
+
+character-reference-invalid@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
+  integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
+
 chardet@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -1880,6 +2245,13 @@ clone-deep@^4.0.1:
     kind-of "^6.0.2"
     shallow-clone "^3.0.0"
 
+clone-regexp@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
+  integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==
+  dependencies:
+    is-regexp "^2.0.0"
+
 clone@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
@@ -1898,6 +2270,11 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
+collapse-white-space@^1.0.2:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
+  integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -1911,10 +2288,22 @@ color-convert@^1.3.0, color-convert@^1.9.0:
   dependencies:
     color-name "1.1.3"
 
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
 color-name@1.1.3, color-name@^1.0.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
 
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
 color-string@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
@@ -1929,6 +2318,11 @@ color@^0.11.0:
     color-convert "^1.3.0"
     color-string "^0.3.0"
 
+colorette@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
+  integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
+
 colormin@^1.0.5:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
@@ -2123,6 +2517,17 @@ cosmiconfig@^5.0.0:
     js-yaml "^3.13.1"
     parse-json "^4.0.0"
 
+cosmiconfig@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
+  integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.1.0"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.7.2"
+
 create-ecdh@^4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
@@ -2236,6 +2641,11 @@ cssesc@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
 
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
 cssnano@^3.10.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
@@ -2365,13 +2775,21 @@ debug@^3.1.0:
   dependencies:
     ms "^2.1.1"
 
-debug@^4.0.1, debug@^4.1.0:
+debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
   dependencies:
     ms "^2.1.1"
 
-decamelize@^1.1.2, decamelize@^1.2.0:
+decamelize-keys@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
+  integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
+  dependencies:
+    decamelize "^1.1.0"
+    map-obj "^1.0.0"
+
+decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
 
@@ -2502,6 +2920,13 @@ dijkstrajs@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
 
+dir-glob@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+  dependencies:
+    path-type "^4.0.0"
+
 doctrine@1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -2545,7 +2970,7 @@ domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
 
-domelementtype@1:
+domelementtype@1, domelementtype@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
 
@@ -2559,6 +2984,13 @@ domhandler@2.1:
   dependencies:
     domelementtype "1"
 
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+  dependencies:
+    domelementtype "1"
+
 domutils@1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
@@ -2572,6 +3004,14 @@ domutils@1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
 duplexer2@~0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -2612,6 +3052,11 @@ electron-to-chromium@^1.3.322:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz#a6f7e1c79025c2b05838e8e344f6e89eb83213a8"
   integrity sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==
 
+electron-to-chromium@^1.3.488:
+  version "1.3.509"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.509.tgz#830fcb89cd66dc2984d18d794973b99e3f00584c"
+  integrity sha512-cN4lkjNRuTG8rtAqTOVgwpecEC2kbKA04PG6YijcKGHK/kD0xLjiqExcAOmLUwtXZRF8cBeam2I0VZcih919Ug==
+
 elliptic@^6.0.0:
   version "6.4.1"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a"
@@ -2628,6 +3073,11 @@ emoji-regex@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
 
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
 emojis-list@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
@@ -2698,7 +3148,7 @@ ent@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
 
-entities@~1.1.1:
+entities@^1.1.1, entities@~1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
 
@@ -2745,6 +3195,11 @@ es6-promisify@^5.0.0:
   dependencies:
     es6-promise "^4.0.3"
 
+escalade@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4"
+  integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==
+
 escape-html@^1.0.3, escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -3004,6 +3459,13 @@ execa@^1.0.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+execall@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45"
+  integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==
+  dependencies:
+    clone-regexp "^2.1.0"
+
 exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -3143,6 +3605,23 @@ fast-deep-equal@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
 
+fast-deep-equal@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-glob@^3.1.1:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
+  integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.0"
+    merge2 "^1.3.0"
+    micromatch "^4.0.2"
+    picomatch "^2.2.1"
+
 fast-json-stable-stringify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@@ -3155,6 +3634,13 @@ fastparse@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
 
+fastq@^1.6.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
+  integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==
+  dependencies:
+    reusify "^1.0.4"
+
 fd-slicer@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
@@ -3211,6 +3697,13 @@ fill-range@^4.0.0:
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
 finalhandler@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
@@ -3270,6 +3763,14 @@ find-up@^3.0.0:
   dependencies:
     locate-path "^3.0.0"
 
+find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
 flat-cache@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@@ -3433,6 +3934,11 @@ gauge@~2.7.3:
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
+gensync@^1.0.0-beta.1:
+  version "1.0.0-beta.1"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+
 get-caller-file@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
@@ -3441,6 +3947,11 @@ get-stdin@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
 
+get-stdin@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
+  integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
+
 get-stream@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@@ -3488,6 +3999,13 @@ glob-parent@^3.1.0:
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
+glob-parent@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob@7.0.5:
   version "7.0.5"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95"
@@ -3542,6 +4060,22 @@ glob@^7.1.2:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+global-modules@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
+  integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
+  dependencies:
+    global-prefix "^3.0.0"
+
+global-prefix@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
+  integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
+  dependencies:
+    ini "^1.3.5"
+    kind-of "^6.0.2"
+    which "^1.3.1"
+
 globals@^11.1.0, globals@^11.7.0:
   version "11.12.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -3550,6 +4084,18 @@ globals@^9.18.0:
   version "9.18.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
 
+globby@^11.0.1:
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
+  integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==
+  dependencies:
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.1.1"
+    ignore "^5.1.4"
+    merge2 "^1.3.0"
+    slash "^3.0.0"
+
 globby@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
@@ -3560,6 +4106,18 @@ globby@^6.1.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
+globjoin@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
+  integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=
+
+gonzales-pe@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
+  integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
+  dependencies:
+    minimist "^1.2.5"
+
 graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
   version "4.1.15"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
@@ -3598,6 +4156,11 @@ har-validator@~5.1.0:
     ajv "^6.5.5"
     har-schema "^2.0.0"
 
+hard-rejection@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
+  integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -3626,6 +4189,11 @@ has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
 
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
 has-symbols@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
@@ -3737,6 +4305,11 @@ html-tags@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b"
   integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=
 
+html-tags@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
+  integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
+
 html-webpack-plugin@^3.0.0, html-webpack-plugin@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
@@ -3749,6 +4322,18 @@ html-webpack-plugin@^3.0.0, html-webpack-plugin@^3.2.0:
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
+htmlparser2@^3.10.0:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+  dependencies:
+    domelementtype "^1.3.1"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^3.1.1"
+
 htmlparser2@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
@@ -3860,6 +4445,11 @@ ignore@^4.0.2, ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
 
+ignore@^5.1.4, ignore@^5.1.8:
+  version "5.1.8"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
+  integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
+
 immediate@~3.0.5:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
@@ -3886,6 +4476,14 @@ import-fresh@^3.0.0:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
 
+import-fresh@^3.1.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
+  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
 import-from@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
@@ -3893,6 +4491,11 @@ import-from@^2.1.0:
   dependencies:
     resolve-from "^3.0.0"
 
+import-lazy@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
+  integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
+
 imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@@ -3903,6 +4506,11 @@ indent-string@^2.1.0:
   dependencies:
     repeating "^2.0.0"
 
+indent-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -3926,12 +4534,12 @@ inherits@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
 
-inherits@~2.0.0:
+inherits@^2.0.0, inherits@~2.0.0:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-ini@~1.3.0:
+ini@^1.3.5, ini@~1.3.0:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
@@ -4005,6 +4613,24 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-alphabetical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
+  integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
+
+is-alphanumeric@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4"
+  integrity sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=
+
+is-alphanumerical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
+  integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
+  dependencies:
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -4019,6 +4645,11 @@ is-buffer@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
 
+is-buffer@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
+  integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
+
 is-builtin-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
@@ -4045,6 +4676,11 @@ is-date-object@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
 
+is-decimal@^1.0.0, is-decimal@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
+  integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
+
 is-descriptor@^0.1.0:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@@ -4109,6 +4745,11 @@ is-fullwidth-code-point@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
 
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
 is-glob@^2.0.0, is-glob@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -4121,12 +4762,17 @@ is-glob@^3.1.0:
   dependencies:
     is-extglob "^2.1.0"
 
-is-glob@^4.0.0:
+is-glob@^4.0.0, is-glob@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
   dependencies:
     is-extglob "^2.1.1"
 
+is-hexadecimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
+  integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
+
 is-number@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
@@ -4147,6 +4793,11 @@ is-number@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
 
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
 is-path-cwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
@@ -4163,10 +4814,15 @@ is-path-inside@^1.0.0:
   dependencies:
     path-is-inside "^1.0.1"
 
-is-plain-obj@^1.0.0:
+is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
 
+is-plain-obj@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
 is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -4191,6 +4847,11 @@ is-regex@^1.0.4:
   dependencies:
     has "^1.0.1"
 
+is-regexp@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
+  integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
+
 is-stream@^1.0.1, is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -4207,7 +4868,7 @@ is-symbol@^1.0.2:
   dependencies:
     has-symbols "^1.0.0"
 
-is-typedarray@~1.0.0:
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
 
@@ -4219,10 +4880,20 @@ is-utf8@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
 
+is-whitespace-character@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
+  integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==
+
 is-windows@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
 
+is-word-character@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230"
+  integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==
+
 is-wsl@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
@@ -4418,6 +5089,13 @@ json5@^2.1.0:
   dependencies:
     minimist "^1.2.0"
 
+json5@^2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+  dependencies:
+    minimist "^1.2.5"
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -4533,12 +5211,27 @@ kind-of@^6.0.0, kind-of@^6.0.2:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
 
+kind-of@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+known-css-properties@^0.19.0:
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b"
+  integrity sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==
+
 lcid@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
   dependencies:
     invert-kv "^2.0.0"
 
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
 levn@^0.3.0, levn@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -4552,6 +5245,11 @@ lie@3.1.1:
   dependencies:
     immediate "~3.0.5"
 
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
 listenercount@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
@@ -4624,6 +5322,13 @@ locate-path@^3.0.0:
     p-locate "^3.0.0"
     path-exists "^3.0.0"
 
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
 lodash._arraycopy@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
@@ -4877,18 +5582,30 @@ lodash@^4.17.13:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
+  version "4.17.19"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+  integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
+
 log-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
   dependencies:
     chalk "^1.0.0"
 
-log-symbols@^2.1.0:
+log-symbols@^2.1.0, log-symbols@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
   dependencies:
     chalk "^2.0.1"
 
+log-symbols@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
+  integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
+  dependencies:
+    chalk "^4.0.0"
+
 log4js@^3.0.0:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff"
@@ -4903,6 +5620,11 @@ lolex@^1.4.0, lolex@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
 
+longest-streak@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
+  integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
+
 loose-envify@^1.0.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -4962,12 +5684,29 @@ map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
 
+map-obj@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5"
+  integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==
+
 map-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
   dependencies:
     object-visit "^1.0.0"
 
+markdown-escapes@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
+  integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
+
+markdown-table@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
+  integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==
+  dependencies:
+    repeat-string "^1.0.0"
+
 math-expression-evaluator@^1.2.14:
   version "1.2.17"
   resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
@@ -4976,6 +5715,11 @@ math-random@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
 
+mathml-tag-names@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
+  integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
+
 md5.js@^1.3.4:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -4984,6 +5728,13 @@ md5.js@^1.3.4:
     inherits "^2.0.1"
     safe-buffer "^5.1.2"
 
+mdast-util-compact@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490"
+  integrity sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==
+  dependencies:
+    unist-util-visit "^2.0.0"
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -5018,10 +5769,34 @@ meow@^3.3.0:
     redent "^1.0.0"
     trim-newlines "^1.0.0"
 
+meow@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-7.0.1.tgz#1ed4a0a50b3844b451369c48362eb0515f04c1dc"
+  integrity sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==
+  dependencies:
+    "@types/minimist" "^1.2.0"
+    arrify "^2.0.1"
+    camelcase "^6.0.0"
+    camelcase-keys "^6.2.2"
+    decamelize-keys "^1.1.0"
+    hard-rejection "^2.1.0"
+    minimist-options "^4.0.2"
+    normalize-package-data "^2.5.0"
+    read-pkg-up "^7.0.1"
+    redent "^3.0.0"
+    trim-newlines "^3.0.0"
+    type-fest "^0.13.1"
+    yargs-parser "^18.1.3"
+
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
 
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -5062,6 +5837,14 @@ micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
+micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
 miller-rabin@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -5101,6 +5884,11 @@ mimic-fn@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
 
+min-indent@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+
 mini-css-extract-plugin@^0.5.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
@@ -5129,6 +5917,15 @@ minimatch@3.0.3:
   dependencies:
     brace-expansion "^1.0.0"
 
+minimist-options@^4.0.2:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
+  integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
+  dependencies:
+    arrify "^1.0.1"
+    is-plain-obj "^1.1.0"
+    kind-of "^6.0.3"
+
 minimist@0.0.8, minimist@~0.0.1:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -5137,6 +5934,11 @@ minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
+minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
 minipass@^2.2.1, minipass@^2.3.4:
   version "2.3.5"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
@@ -5382,6 +6184,11 @@ node-releases@^1.1.42:
   dependencies:
     semver "^6.3.0"
 
+node-releases@^1.1.58:
+  version "1.1.60"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
+  integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==
+
 nomnomnomnom@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/nomnomnomnom/-/nomnomnomnom-2.0.1.tgz#b2239f031c8d04da67e32836e1e3199e12f7a8e2"
@@ -5411,6 +6218,16 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-package-data@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+  integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+  dependencies:
+    hosted-git-info "^2.1.4"
+    resolve "^1.10.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
 normalize-path@^2.0.1, normalize-path@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@@ -5425,6 +6242,11 @@ normalize-range@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
 
+normalize-selector@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
+  integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
+
 normalize-url@^1.4.0:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
@@ -5645,6 +6467,13 @@ p-limit@^2.0.0:
   dependencies:
     p-try "^2.0.0"
 
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
 p-locate@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -5657,6 +6486,13 @@ p-locate@^3.0.0:
   dependencies:
     p-limit "^2.0.0"
 
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
 p-map@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
@@ -5728,6 +6564,18 @@ parse-asn1@^5.0.0:
     pbkdf2 "^3.0.3"
     safe-buffer "^5.1.1"
 
+parse-entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
+  integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
+  dependencies:
+    character-entities "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    character-reference-invalid "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 parse-glob@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
@@ -5751,6 +6599,16 @@ parse-json@^4.0.0:
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
+parse-json@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.1.tgz#7cfe35c1ccd641bce3981467e6c2ece61b3b3878"
+  integrity sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+    lines-and-columns "^1.1.6"
+
 parse-link-header@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7"
@@ -5796,6 +6654,11 @@ path-exists@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
 
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
 path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -5836,6 +6699,11 @@ path-type@^2.0.0:
   dependencies:
     pify "^2.0.0"
 
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
 pbkdf2@^3.0.3:
   version "3.0.17"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
@@ -5858,6 +6726,11 @@ phoenix@^1.3.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.4.0.tgz#9cec8dbd8cbc59ecd2147bc09ca8ceb56b860d75"
 
+picomatch@^2.0.5, picomatch@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -5977,6 +6850,20 @@ postcss-filter-plugins@^2.0.0:
   dependencies:
     postcss "^5.0.4"
 
+postcss-html@^0.36.0:
+  version "0.36.0"
+  resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204"
+  integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==
+  dependencies:
+    htmlparser2 "^3.10.0"
+
+postcss-less@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad"
+  integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==
+  dependencies:
+    postcss "^7.0.14"
+
 postcss-load-config@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a"
@@ -6018,6 +6905,11 @@ postcss-loader@^3.0.0:
     postcss-load-config "^2.0.0"
     schema-utils "^1.0.0"
 
+postcss-media-query-parser@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
+  integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=
+
 postcss-merge-idents@^2.1.5:
   version "2.1.7"
   resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
@@ -6149,6 +7041,52 @@ postcss-reduce-transforms@^1.0.3:
     postcss "^5.0.8"
     postcss-value-parser "^3.0.1"
 
+postcss-reporter@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f"
+  integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==
+  dependencies:
+    chalk "^2.4.1"
+    lodash "^4.17.11"
+    log-symbols "^2.2.0"
+    postcss "^7.0.7"
+
+postcss-resolve-nested-selector@0.1.1, postcss-resolve-nested-selector@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
+  integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=
+
+postcss-safe-parser@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96"
+  integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==
+  dependencies:
+    postcss "^7.0.26"
+
+postcss-sass@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3"
+  integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==
+  dependencies:
+    gonzales-pe "^4.3.0"
+    postcss "^7.0.21"
+
+postcss-scss@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383"
+  integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==
+  dependencies:
+    postcss "^7.0.6"
+
+postcss-selector-parser@2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.1.tgz#fdbf696103b12b0a64060e5610507f410491f7c8"
+  integrity sha1-/b9pYQOxKwpkBg5WEFB/QQSR98g=
+  dependencies:
+    flatten "^1.0.2"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
 postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
@@ -6157,6 +7095,15 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
     indexes-of "^1.0.1"
     uniq "^1.0.1"
 
+postcss-selector-parser@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+  integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+
 postcss-svgo@^2.1.1:
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
@@ -6166,6 +7113,11 @@ postcss-svgo@^2.1.1:
     postcss-value-parser "^3.2.3"
     svgo "^0.7.0"
 
+postcss-syntax@^0.36.2:
+  version "0.36.2"
+  resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c"
+  integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==
+
 postcss-unique-selectors@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d"
@@ -6178,6 +7130,11 @@ postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
 
+postcss-value-parser@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
 postcss-zindex@^2.0.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
@@ -6212,6 +7169,15 @@ postcss@^7.0.0:
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
+postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.6, postcss@^7.0.7:
+  version "7.0.32"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
+  integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
+  dependencies:
+    chalk "^2.4.2"
+    source-map "^0.6.1"
+    supports-color "^6.1.0"
+
 prelude-ls@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -6369,6 +7335,11 @@ querystring@0.2.0, querystring@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
 
+quick-lru@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
+  integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
+
 randomatic@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
@@ -6434,6 +7405,15 @@ read-pkg-up@^2.0.0:
     find-up "^2.0.0"
     read-pkg "^2.0.0"
 
+read-pkg-up@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
+  dependencies:
+    find-up "^4.1.0"
+    read-pkg "^5.2.0"
+    type-fest "^0.8.1"
+
 read-pkg@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -6450,6 +7430,16 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
+read-pkg@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
 "readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
@@ -6480,6 +7470,15 @@ readable-stream@1.1.x:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
+readable-stream@^3.1.1:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
 readdirp@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -6501,6 +7500,14 @@ redent@^1.0.0:
     indent-string "^2.1.0"
     strip-indent "^1.0.1"
 
+redent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
+  integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+  dependencies:
+    indent-string "^4.0.0"
+    strip-indent "^3.0.0"
+
 reduce-css-calc@^1.2.6:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
@@ -6605,6 +7612,57 @@ relateurl@0.2.x:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
 
+remark-parse@^8.0.0:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
+  integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==
+  dependencies:
+    ccount "^1.0.0"
+    collapse-white-space "^1.0.2"
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-whitespace-character "^1.0.0"
+    is-word-character "^1.0.0"
+    markdown-escapes "^1.0.0"
+    parse-entities "^2.0.0"
+    repeat-string "^1.5.4"
+    state-toggle "^1.0.0"
+    trim "0.0.1"
+    trim-trailing-lines "^1.0.0"
+    unherit "^1.0.4"
+    unist-util-remove-position "^2.0.0"
+    vfile-location "^3.0.0"
+    xtend "^4.0.1"
+
+remark-stringify@^8.0.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.1.1.tgz#e2a9dc7a7bf44e46a155ec78996db896780d8ce5"
+  integrity sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A==
+  dependencies:
+    ccount "^1.0.0"
+    is-alphanumeric "^1.0.0"
+    is-decimal "^1.0.0"
+    is-whitespace-character "^1.0.0"
+    longest-streak "^2.0.1"
+    markdown-escapes "^1.0.0"
+    markdown-table "^2.0.0"
+    mdast-util-compact "^2.0.0"
+    parse-entities "^2.0.0"
+    repeat-string "^1.5.4"
+    state-toggle "^1.0.0"
+    stringify-entities "^3.0.0"
+    unherit "^1.0.4"
+    xtend "^4.0.1"
+
+remark@^12.0.0:
+  version "12.0.1"
+  resolved "https://registry.yarnpkg.com/remark/-/remark-12.0.1.tgz#f1ddf68db7be71ca2bad0a33cd3678b86b9c709f"
+  integrity sha512-gS7HDonkdIaHmmP/+shCPejCEEW+liMp/t/QwmF0Xt47Rpuhl32lLtDV1uKWvGoq+kxr5jSgg5oAIpGuyULjUw==
+  dependencies:
+    remark-parse "^8.0.0"
+    remark-stringify "^8.0.0"
+    unified "^9.0.0"
+
 remove-trailing-separator@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -6627,7 +7685,7 @@ repeat-string@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae"
 
-repeat-string@^1.5.2, repeat-string@^1.6.1:
+repeat-string@^1.0.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
 
@@ -6637,6 +7695,11 @@ repeating@^2.0.0:
   dependencies:
     is-finite "^1.0.0"
 
+replace-ext@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
+  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
+
 request@^2.88.0:
   version "2.88.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@@ -6691,6 +7754,11 @@ resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
 
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -6730,6 +7798,11 @@ ret@~0.1.10:
   version "0.1.15"
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
 
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
 rfdc@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
@@ -6760,6 +7833,11 @@ run-async@^2.2.0:
   dependencies:
     is-promise "^2.1.0"
 
+run-parallel@^1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
+  integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
+
 run-queue@^1.0.0, run-queue@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
@@ -6776,6 +7854,11 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 
+safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
 safe-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -6969,6 +8052,11 @@ slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
 
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
 slice-ansi@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@@ -7151,6 +8239,11 @@ spdx-license-ids@^3.0.0:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e"
 
+specificity@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
+  integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
+
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -7181,6 +8274,11 @@ ssri@^6.0.1:
   dependencies:
     figgy-pudding "^3.5.1"
 
+state-toggle@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
+  integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==
+
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -7260,12 +8358,28 @@ string-width@^3.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
+string-width@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.0"
+
 string_decoder@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
   dependencies:
     safe-buffer "~5.1.0"
 
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
 string_decoder@~0.10.x:
   version "0.10.31"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -7276,6 +8390,17 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
+stringify-entities@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.1.tgz#32154b91286ab0869ab2c07696223bd23b6dbfc0"
+  integrity sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ==
+  dependencies:
+    character-entities-html4 "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.2"
+    is-hexadecimal "^1.0.0"
+
 strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -7294,6 +8419,13 @@ strip-ansi@^5.1.0:
   dependencies:
     ansi-regex "^4.1.0"
 
+strip-ansi@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+  integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
+  dependencies:
+    ansi-regex "^5.0.0"
+
 strip-ansi@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
@@ -7318,10 +8450,103 @@ strip-indent@^1.0.1:
   dependencies:
     get-stdin "^4.0.1"
 
+strip-indent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
+  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
+  dependencies:
+    min-indent "^1.0.0"
+
 strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
+style-search@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
+  integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
+
+stylelint-config-recommended@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657"
+  integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==
+
+stylelint-config-standard@^20.0.0:
+  version "20.0.0"
+  resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz#06135090c9e064befee3d594289f50e295b5e20d"
+  integrity sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA==
+  dependencies:
+    stylelint-config-recommended "^3.0.0"
+
+stylelint-rscss@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/stylelint-rscss/-/stylelint-rscss-0.4.0.tgz#ea77c478e1c703dbda878c00f53bbc002b2d07b7"
+  integrity sha1-6nfEeOHHA9vah4wA9Tu8ACstB7c=
+  dependencies:
+    postcss-resolve-nested-selector "0.1.1"
+    postcss-selector-parser "2.2.1"
+
+stylelint@^13.6.1:
+  version "13.6.1"
+  resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878"
+  integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw==
+  dependencies:
+    "@stylelint/postcss-css-in-js" "^0.37.1"
+    "@stylelint/postcss-markdown" "^0.36.1"
+    autoprefixer "^9.8.0"
+    balanced-match "^1.0.0"
+    chalk "^4.1.0"
+    cosmiconfig "^6.0.0"
+    debug "^4.1.1"
+    execall "^2.0.0"
+    file-entry-cache "^5.0.1"
+    get-stdin "^8.0.0"
+    global-modules "^2.0.0"
+    globby "^11.0.1"
+    globjoin "^0.1.4"
+    html-tags "^3.1.0"
+    ignore "^5.1.8"
+    import-lazy "^4.0.0"
+    imurmurhash "^0.1.4"
+    known-css-properties "^0.19.0"
+    leven "^3.1.0"
+    lodash "^4.17.15"
+    log-symbols "^4.0.0"
+    mathml-tag-names "^2.1.3"
+    meow "^7.0.1"
+    micromatch "^4.0.2"
+    normalize-selector "^0.2.0"
+    postcss "^7.0.32"
+    postcss-html "^0.36.0"
+    postcss-less "^3.1.4"
+    postcss-media-query-parser "^0.2.3"
+    postcss-reporter "^6.0.1"
+    postcss-resolve-nested-selector "^0.1.1"
+    postcss-safe-parser "^4.0.2"
+    postcss-sass "^0.4.4"
+    postcss-scss "^2.1.1"
+    postcss-selector-parser "^6.0.2"
+    postcss-syntax "^0.36.2"
+    postcss-value-parser "^4.1.0"
+    resolve-from "^5.0.0"
+    slash "^3.0.0"
+    specificity "^0.4.1"
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    style-search "^0.1.0"
+    sugarss "^2.0.0"
+    svg-tags "^1.0.0"
+    table "^5.4.6"
+    v8-compile-cache "^2.1.1"
+    write-file-atomic "^3.0.3"
+
+sugarss@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d"
+  integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==
+  dependencies:
+    postcss "^7.0.2"
+
 supports-color@3.1.2, supports-color@^3.1.0:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
@@ -7350,6 +8575,13 @@ supports-color@^6.1.0:
   dependencies:
     has-flag "^3.0.0"
 
+supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  dependencies:
+    has-flag "^4.0.0"
+
 svg-tags@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
@@ -7376,6 +8608,16 @@ table@^5.2.3:
     slice-ansi "^2.1.0"
     string-width "^3.0.0"
 
+table@^5.4.6:
+  version "5.4.6"
+  resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
+  integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+  dependencies:
+    ajv "^6.10.2"
+    lodash "^4.17.14"
+    slice-ansi "^2.1.0"
+    string-width "^3.0.0"
+
 tapable@^1.0.0, tapable@^1.1.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
@@ -7491,6 +8733,13 @@ to-regex-range@^2.1.0:
     is-number "^3.0.0"
     repeat-string "^1.6.1"
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
 to-regex@^3.0.1, to-regex@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -7520,10 +8769,30 @@ trim-newlines@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
 
+trim-newlines@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
+  integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
+
 trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
 
+trim-trailing-lines@^1.0.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94"
+  integrity sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==
+
+trim@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
+  integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
+
+trough@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
+  integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
+
 tslib@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
@@ -7560,6 +8829,21 @@ type-detect@^4.0.0:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
 
+type-fest@^0.13.1:
+  version "0.13.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
+  integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
+
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
+type-fest@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
 type-is@~1.6.16:
   version "1.6.16"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
@@ -7567,6 +8851,13 @@ type-is@~1.6.16:
     media-typer "0.3.0"
     mime-types "~2.1.18"
 
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+  dependencies:
+    is-typedarray "^1.0.0"
+
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -7586,6 +8877,14 @@ underscore@~1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
 
+unherit@^1.0.4:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
+  integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==
+  dependencies:
+    inherits "^2.0.0"
+    xtend "^4.0.0"
+
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -7609,6 +8908,18 @@ unicode-property-aliases-ecmascript@^1.0.4:
   resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
   integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
 
+unified@^9.0.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/unified/-/unified-9.1.0.tgz#7ba82e5db4740c47a04e688a9ca8335980547410"
+  integrity sha512-VXOv7Ic6twsKGJDeZQ2wwPqXs2hM0KNu5Hkg9WgAZbSD1pxhZ7p8swqg583nw1Je2fhwHy6U8aEjiI79x1gvag==
+  dependencies:
+    bail "^1.0.0"
+    extend "^3.0.0"
+    is-buffer "^2.0.0"
+    is-plain-obj "^2.0.0"
+    trough "^1.0.0"
+    vfile "^4.0.0"
+
 union-value@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@@ -7638,6 +8949,49 @@ unique-slug@^2.0.0:
   dependencies:
     imurmurhash "^0.1.4"
 
+unist-util-find-all-after@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.1.tgz#95cc62f48812d879b4685a0512bf1b838da50e9a"
+  integrity sha512-0GICgc++sRJesLwEYDjFVJPJttBpVQaTNgc6Jw0Jhzvfs+jtKePEMu+uD+PqkRUrAvGQqwhpDwLGWo1PK8PDEw==
+  dependencies:
+    unist-util-is "^4.0.0"
+
+unist-util-is@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.2.tgz#c7d1341188aa9ce5b3cff538958de9895f14a5de"
+  integrity sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==
+
+unist-util-remove-position@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc"
+  integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==
+  dependencies:
+    unist-util-visit "^2.0.0"
+
+unist-util-stringify-position@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da"
+  integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==
+  dependencies:
+    "@types/unist" "^2.0.2"
+
+unist-util-visit-parents@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5"
+  integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-is "^4.0.0"
+
+unist-util-visit@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
+  integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-is "^4.0.0"
+    unist-util-visit-parents "^3.0.0"
+
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -7709,7 +9063,7 @@ useragent@2.3.0:
     lru-cache "4.1.x"
     tmp "0.0.x"
 
-util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
 
@@ -7748,6 +9102,11 @@ v-click-outside@^2.1.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7"
 
+v8-compile-cache@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
+  integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -7771,6 +9130,30 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+vfile-location@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.0.1.tgz#d78677c3546de0f7cd977544c367266764d31bb3"
+  integrity sha512-yYBO06eeN/Ki6Kh1QAkgzYpWT1d3Qln+ZCtSbJqFExPl1S3y2qqotJQXoh6qEvl/jDlgpUJolBn3PItVnnZRqQ==
+
+vfile-message@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
+  integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-stringify-position "^2.0.0"
+
+vfile@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.1.1.tgz#282d28cebb609183ac51703001bc18b3e3f17de9"
+  integrity sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    is-buffer "^2.0.0"
+    replace-ext "1.0.0"
+    unist-util-stringify-position "^2.0.0"
+    vfile-message "^2.0.0"
+
 vm-browserify@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
@@ -7952,7 +9335,7 @@ which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
 
-which@^1.0.9, which@^1.1.1, which@^1.2.9:
+which@^1.0.9, which@^1.1.1, which@^1.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   dependencies:
@@ -7993,6 +9376,16 @@ wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 
+write-file-atomic@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+  integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+  dependencies:
+    imurmurhash "^0.1.4"
+    is-typedarray "^1.0.0"
+    signal-exit "^3.0.2"
+    typedarray-to-buffer "^3.1.5"
+
 write@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
@@ -8019,6 +9412,11 @@ xtend@^4.0.0, xtend@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
 
+xtend@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
 "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
@@ -8031,6 +9429,11 @@ yallist@^3.0.0, yallist@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
 
+yaml@^1.7.2:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
+  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+
 yargs-parser@^11.1.1:
   version "11.1.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
@@ -8038,6 +9441,14 @@ yargs-parser@^11.1.1:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
+yargs-parser@^18.1.3:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
 yargs@^12.0.5:
   version "12.0.5"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
-- 
cgit v1.2.3-70-g09d2


From b5d15eddcf6bb3a9dac4f4b3d4a063aa84a8c693 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Mon, 31 Aug 2020 11:50:26 +0300
Subject: hide poll when subject collapsed, but show poll icon

---
 src/components/status_content/status_content.vue | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

(limited to 'src/components/status_content/status_content.vue')

diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index fb469a2f..76fe3278 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -71,6 +71,10 @@
           v-if="attachmentTypes.includes('unknown')"
           class="icon-doc"
         />
+        <span
+          v-if="status.poll && status.poll.options"
+          class="icon-chart-bar"
+        />
         <span
           v-if="status.card"
           class="icon-link"
@@ -86,7 +90,7 @@
       </a>
     </div>
 
-    <div v-if="status.poll && status.poll.options">
+    <div v-if="status.poll && status.poll.options && !hideSubjectStatus">
       <poll :base-poll="status.poll" />
     </div>
 
-- 
cgit v1.2.3-70-g09d2

