From 709b75198d4e7051c7e1d310379bfeddad5c411f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 25 Apr 2021 13:45:47 +0300
Subject: fix portals/teleports

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

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 1a166778..702b69d1 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -58,7 +58,7 @@
     <UserReportingModal />
     <PostStatusModal />
     <SettingsModal />
-    <portal-target name="modal" />
+    <div id="modal"/>
     <GlobalNoticeList />
   </div>
 </template>
-- 
cgit v1.2.3-70-g09d2


From be4244acde8183271d3770f2fbbd9d1ac571086b Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 17 Mar 2022 08:35:19 +0200
Subject: fix some warnings

---
 src/App.vue                                        |   2 +-
 src/components/basic_user_card/basic_user_card.vue |   2 +-
 src/components/chat/chat.vue                       | 118 ++++++++++-----------
 src/components/checkbox/checkbox.vue               |   2 +-
 src/components/desktop_nav/desktop_nav.vue         |   2 +-
 src/components/font_control/font_control.js        |   2 +-
 src/components/importer/importer.vue               |   4 +-
 src/components/status/status.vue                   |   2 +-
 8 files changed, 66 insertions(+), 68 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 31e8fd40..3532f898 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -59,7 +59,7 @@
     <UserReportingModal />
     <PostStatusModal />
     <SettingsModal />
-    <div id="modal"/>
+    <div id="modal" />
     <GlobalNoticeList />
   </div>
 </template>
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 53deb1df..eeca7828 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -4,7 +4,7 @@
       <UserAvatar
         class="avatar"
         :user="user"
-        @click.prevent.native="toggleUserExpanded"
+        @click.prevent="toggleUserExpanded"
       />
     </router-link>
     <div
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 94a0097c..493c5d5a 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -26,73 +26,71 @@
             />
           </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
-            ref="scrollable"
-            class="scrollable-message-list"
-            :style="{ height: scrollableContainerHeight }"
-            @scroll="handleScroll"
+            v-else
+            class="chat-loading-error"
           >
-            <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 class="alert error">
+              {{ $t('chats.error_loading_chat') }}
             </div>
           </div>
+        </div>
+        <div
+          ref="footer"
+          class="panel-body footer"
+        >
           <div
-            ref="footer"
-            class="panel-body footer"
+            class="jump-to-bottom-button"
+            :class="{ 'visible': jumpToBottomButtonVisible }"
+            @click="scrollDown({ behavior: 'smooth' })"
           >
-            <div
-              class="jump-to-bottom-button"
-              :class="{ 'visible': jumpToBottomButtonVisible }"
-              @click="scrollDown({ behavior: 'smooth' })"
-            >
-              <span>
-                <FAIcon icon="chevron-down" />
-                <div
-                  v-if="newMessageCount"
-                  class="badge badge-notification unread-chat-count unread-message-count"
-                >
-                  {{ newMessageCount }}
-                </div>
-              </span>
-            </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"
-              :disable-preview="true"
-              :optimistic-posting="true"
-              :post-handler="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"
-            />
+            <span>
+              <FAIcon icon="chevron-down" />
+              <div
+                v-if="newMessageCount"
+                class="badge badge-notification unread-chat-count unread-message-count"
+              >
+                {{ newMessageCount }}
+              </div>
+            </span>
           </div>
-        </template>
+          <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"
+            :disable-preview="true"
+            :optimistic-posting="true"
+            :post-handler="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>
       </div>
     </div>
   </div>
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index d28c2cfd..ff2b4e77 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -7,7 +7,7 @@
       type="checkbox"
       :disabled="disabled"
       :checked="checked"
-      :indeterminate.prop="indeterminate"
+      :indeterminate="indeterminate"
       @change="$emit('change', $event.target.checked)"
     >
     <i class="checkbox-indicator" />
diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue
index 304baf9d..bab3ca81 100644
--- a/src/components/desktop_nav/desktop_nav.vue
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -34,7 +34,7 @@
         <search-bar
           v-if="currentUser || !privateMode"
           @toggled="onSearchBarToggled"
-          @click.stop.native
+          @click.stop
         />
         <button
           class="button-unstyled nav-icon"
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
index 137ef9c0..24bad937 100644
--- a/src/components/font_control/font_control.js
+++ b/src/components/font_control/font_control.js
@@ -1,4 +1,4 @@
-import { set } from 'vue'
+import { set } from 'lodash'
 import Select from '../select/select.vue'
 
 export default {
diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue
index 466e15b9..ddc3010e 100644
--- a/src/components/importer/importer.vue
+++ b/src/components/importer/importer.vue
@@ -25,14 +25,14 @@
         icon="times"
         @click="dismiss"
       />
-      <p>{{ successMessage || $t('importer.success')}}</p>
+      <p>{{ successMessage || $t('importer.success') }}</p>
     </div>
     <div v-else-if="error">
       <FAIcon
         icon="times"
         @click="dismiss"
       />
-      <p>{{ errorMessage || $t('importer.error')}}</p>
+      <p>{{ errorMessage || $t('importer.error') }}</p>
     </div>
   </div>
 </template>
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index f5b71f54..f778753f 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -123,7 +123,7 @@
         >
           <router-link
             :to="userProfileLink"
-            @click.stop.prevent.capture.native="toggleUserExpanded"
+            @click.stop.prevent.capture="toggleUserExpanded"
           >
             <UserAvatar
               :bot="botIndicator"
-- 
cgit v1.2.3-70-g09d2


From a6fae395dac4f56ba0a48880f0ce2500b5dd5ebd Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 23 Mar 2022 16:05:53 +0200
Subject: fix dupe id

---
 src/App.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 3532f898..bee98751 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,11 +1,11 @@
 <template>
   <div
-    id="app"
-    :style="bgStyle"
+    id="app-loaded"
   >
     <div
       id="app_bg_wrapper"
       class="app-bg-wrapper"
+      :style="bgStyle"
     />
     <MobileNav v-if="isMobileLayout" />
     <DesktopNav v-else />
-- 
cgit v1.2.3-70-g09d2


From 805615d52befc413693c5a80d69e2f901d106076 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 28 Mar 2022 14:26:50 +0300
Subject: fix background not showing

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

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index bee98751..b18b3308 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,11 +1,11 @@
 <template>
   <div
     id="app-loaded"
+    :style="bgStyle"
   >
     <div
       id="app_bg_wrapper"
       class="app-bg-wrapper"
-      :style="bgStyle"
     />
     <MobileNav v-if="isMobileLayout" />
     <DesktopNav v-else />
-- 
cgit v1.2.3-70-g09d2


From 0e83ced25b612250d8126778288b7b44f3b91dfc Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 4 Apr 2022 09:42:52 +0300
Subject: refactored how main app layout works

---
 src/App.scss                                | 100 ++++++++++++++++++++--------
 src/App.vue                                 |  31 ++++-----
 src/components/desktop_nav/desktop_nav.scss |   2 -
 src/components/mobile_nav/mobile_nav.vue    |   3 +-
 4 files changed, 88 insertions(+), 48 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.scss b/src/App.scss
index 180c0daf..fa2c6a79 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,9 +1,19 @@
 @import './_variables.scss';
 
-#app {
+body {
+  overflow: hidden;
+}
+
+#app-loaded {
   min-height: 100vh;
-  max-width: 100%;
+  min-width: 100vw;
   overflow: hidden;
+
+  --navbar-height: 50px;
+}
+
+.navbar {
+  height: var(--navbar-height);
 }
 
 .app-bg-wrapper {
@@ -16,11 +26,7 @@
   background-repeat: no-repeat;
   background-color: var(--wallpaper);
   background-image: var(--body-background-image);
-  background-position: 50% 50px;
-}
-
-i[class^='icon-'] {
-  user-select: none;
+  background-position: 50%;
 }
 
 h4 {
@@ -28,12 +34,69 @@ h4 {
 }
 
 #content {
+  overflow-y: auto;
+  position: sticky;
+}
+
+.app-layout {
+  position: relative;
+  display: grid;
+  grid-template-columns: auto auto;
+  grid-template-rows: 1fr;
   box-sizing: border-box;
-  padding-top: 60px;
-  margin: auto;
-  min-height: 100vh;
-  max-width: 980px;
+  margin: 0 auto;
+  height: 100vh;
   align-content: flex-start;
+  flex-wrap: wrap;
+  padding: 0 10px 0 10px;
+  grid-template-columns: auto auto;
+  justify-content: center;
+}
+
+.underlay {
+  height: 100%;
+  width: 100%;
+  grid-column-start: 1;
+  grid-column-end: 3;
+  grid-row-start: 1;
+  grid-row-end: 1;
+  margin: -0.5em;
+  padding: 0.5em;
+}
+
+.column {
+  max-width: 615px;
+  padding-top: 10px;
+  grid-row-start: 1;
+  grid-row-end: 1;
+
+  &:nth-child(2) {
+    grid-column: 1;
+  }
+
+  &:nth-child(3) {
+    grid-column: 2;
+  }
+
+  &.-mini {
+    max-width: 345px;
+  }
+
+  &.-scrollable {
+    position: sticky;
+    top: 0;
+    margin-top: calc(-1 * var(--navbar-padding));
+    max-height: 100vh;
+    overflow-y: auto;
+  }
+}
+
+body,
+.column.-scrollable {
+  &::-webkit-scrollbar {
+    display: block;
+    width: 0;
+  }
 }
 
 .underlay {
@@ -362,13 +425,6 @@ i[class*=icon-], .svg-inline--fa  {
   }
 }
 
-.container {
-  display: flex;
-  flex-wrap: wrap;
-  margin: 0;
-  padding: 0 10px 0 10px;
-}
-
 .auto-size {
   flex: 1
 }
@@ -598,10 +654,6 @@ nav {
 }
 
 @media all and (min-width: 800px) {
-  body {
-    overflow-y: scroll;
-  }
-
   .sidebar-bounds {
     overflow: hidden;
     max-height: 100vh;
@@ -856,10 +908,6 @@ nav {
   height: 100%;
 
   // Get rid of scrollbar on body as scrolling happens on different element
-  body {
-    overflow: hidden;
-  }
-
   // Ensures the fixed position of the mobile browser bars on scroll up / down events.
   // Prevents the mobile browser bars from overlapping or hiding the message posting form.
   @media all and (max-width: 800px) {
diff --git a/src/App.vue b/src/App.vue
index b18b3308..dc2359f3 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -7,33 +7,28 @@
       id="app_bg_wrapper"
       class="app-bg-wrapper"
     />
-    <MobileNav v-if="isMobileLayout" />
-    <DesktopNav v-else />
+    <MobileNav class="navbar" v-if="isMobileLayout" />
+    <DesktopNav class="navbar" v-else />
     <div class="app-bg-wrapper app-container-wrapper" />
     <div
       id="content"
-      class="container underlay"
+      class="app-layout container"
     >
+      <div class="underlay"/>
       <div
-        class="sidebar-flexer mobile-hidden"
+        class="column -scrollable -mini mobile-hidden"
         :style="sidebarAlign"
       >
-        <div class="sidebar-bounds">
-          <div class="sidebar-scroller">
-            <div class="sidebar">
-              <user-panel />
-              <div v-if="!isMobileLayout">
-                <nav-panel />
-                <instance-specific-panel v-if="showInstanceSpecificPanel" />
-                <features-panel v-if="!currentUser && showFeaturesPanel" />
-                <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
-                <notifications v-if="currentUser" />
-              </div>
-            </div>
-          </div>
+        <user-panel />
+        <div v-if="!isMobileLayout">
+          <nav-panel />
+          <instance-specific-panel v-if="showInstanceSpecificPanel" />
+          <features-panel v-if="!currentUser && showFeaturesPanel" />
+          <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
+          <notifications v-if="currentUser" />
         </div>
       </div>
-      <div class="main">
+      <div class="column main">
         <div
           v-if="!currentUser"
           class="login-hint panel panel-default"
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index 2d468588..1e731c70 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -1,9 +1,7 @@
 @import '../../_variables.scss';
 
 .DesktopNav {
-  height: 50px;
   width: 100%;
-  position: fixed;
 
   a {
     color: var(--topBarLink, $fallback--link);
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 0f0ea457..229f3006 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -92,8 +92,7 @@
 .MobileNav {
   .mobile-nav {
     display: grid;
-    line-height: 50px;
-    height: 50px;
+    line-height: var(--navbar-height);
     grid-template-rows: 50px;
     grid-template-columns: 2fr auto;
     width: 100%;
-- 
cgit v1.2.3-70-g09d2


From 71863061c691df7f59cf4be283a3e0772d485378 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 4 Apr 2022 19:41:09 +0300
Subject: fixed tons of stuff, at least it looks normalish on desktop

---
 src/App.js                                        |   6 +-
 src/App.scss                                      | 515 +++++++++++-----------
 src/App.vue                                       |  14 +-
 src/components/chat/chat.js                       |  46 +-
 src/components/chat/chat.scss                     |  16 +-
 src/components/chat/chat.vue                      |   6 +-
 src/components/chat/chat_layout_utils.js          |   1 +
 src/components/mobile_nav/mobile_nav.vue          |   2 +-
 src/components/notifications/notifications.vue    |   2 +-
 src/components/password_reset/password_reset.vue  |   3 +
 src/components/registration/registration.vue      |   5 +-
 src/components/settings_modal/settings_modal.scss |  12 +
 src/components/status/status.scss                 |   4 +
 src/components/timeline/timeline.js               |   6 +-
 src/components/timeline/timeline.scss             |   1 -
 src/components/user_card/user_card.vue            |   8 +-
 16 files changed, 320 insertions(+), 327 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index c4360af5..4a59f412 100644
--- a/src/App.js
+++ b/src/App.js
@@ -81,11 +81,7 @@ export default {
     },
     isMobileLayout () { return this.$store.state.interface.mobileLayout },
     privateMode () { return this.$store.state.instance.private },
-    sidebarAlign () {
-      return {
-        'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
-      }
-    },
+    reverseLayout () { return this.$store.getters.mergedConfig.sidebarRight },
     ...mapGetters(['mergedConfig'])
   },
   methods: {
diff --git a/src/App.scss b/src/App.scss
index fa2c6a79..7e428407 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,7 +1,51 @@
+// stylelint-disable rscss/class-format
 @import './_variables.scss';
 
+html {
+  font-size: 14px;
+  overflow: hidden;
+  max-height: 100vh;
+}
+
 body {
   overflow: hidden;
+  max-height: 100vh;
+  max-width: 100vw;
+  font-family: sans-serif;
+  font-family: var(--interfaceFont, sans-serif);
+  margin: 0;
+  color: $fallback--text;
+  color: var(--text, $fallback--text);
+  overflow-x: hidden;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+  &.hidden {
+    display: none;
+  }
+}
+
+a {
+  text-decoration: none;
+  color: $fallback--link;
+  color: var(--link, $fallback--link);
+}
+
+h4 {
+  margin: 0;
+}
+
+nav {
+  z-index: 1000;
+  color: var(--topBarText);
+  background-color: $fallback--fg;
+  background-color: var(--topBar, $fallback--fg);
+  color: $fallback--faint;
+  color: var(--faint, $fallback--faint);
+  box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
+  box-shadow: var(--topBarShadow);
+  box-sizing: border-box;
+  height: var(--navbar-height);
 }
 
 #app-loaded {
@@ -12,14 +56,29 @@ body {
   --navbar-height: 50px;
 }
 
-.navbar {
-  height: var(--navbar-height);
+#content {
+  overscroll-behavior-y: none;
+  overflow-y: auto;
+  position: sticky;
+}
+
+#sidebar {
+  display: grid;
+  grid-template-columns: 100%;
+  row-gap: 1em;
+  grid-area: sidebar;
+  align-content: start;
+}
+
+#main-scroller {
+  grid-area: content;
 }
 
 .app-bg-wrapper {
   position: fixed;
-  z-index: -1;
   height: 100%;
+  top: var(--navbar-height);
+  z-index: -1000;
   left: 0;
   right: -20px;
   background-size: cover;
@@ -29,69 +88,66 @@ body {
   background-position: 50%;
 }
 
-h4 {
-  margin: 0;
-}
-
-#content {
-  overflow-y: auto;
-  position: sticky;
-}
-
 .app-layout {
   position: relative;
   display: grid;
-  grid-template-columns: auto auto;
+  grid-template-columns: auto minmax(auto, 1fr);
+  grid-template-areas: "sidebar content";
   grid-template-rows: 1fr;
   box-sizing: border-box;
   margin: 0 auto;
-  height: 100vh;
+  height: calc(100vh - var(--navbar-height));
   align-content: flex-start;
   flex-wrap: wrap;
   padding: 0 10px 0 10px;
-  grid-template-columns: auto auto;
   justify-content: center;
+
+  &.-reverse {
+    grid-template-columns: minmax(auto, 1fr) auto;
+    grid-template-areas: "content sidebar";
+  }
 }
 
 .underlay {
-  height: 100%;
-  width: 100%;
   grid-column-start: 1;
-  grid-column-end: 3;
+  grid-column-end: span 2;
   grid-row-start: 1;
   grid-row-end: 1;
-  margin: -0.5em;
-  padding: 0.5em;
+  margin: 0 -0.5em;
+  padding: 0 0.5em;
+  pointer-events: none;
+  background-color: rgba(0, 0, 0, 0.15);
+  background-color: var(--underlay, rgba(0, 0, 0, 0.15));
+  z-index: -2000;
 }
 
 .column {
+  box-sizing: border-box;
   max-width: 615px;
   padding-top: 10px;
   grid-row-start: 1;
   grid-row-end: 1;
-
-  &:nth-child(2) {
-    grid-column: 1;
-  }
-
-  &:nth-child(3) {
-    grid-column: 2;
-  }
+  margin: 0 0.5em;
 
   &.-mini {
     max-width: 345px;
   }
 
   &.-scrollable {
+    padding-top: 10px;
     position: sticky;
     top: 0;
-    margin-top: calc(-1 * var(--navbar-padding));
-    max-height: 100vh;
+    max-height: calc(100vh - var(--navbar-height));
     overflow-y: auto;
+    overflow-x: hidden;
+
+    .panel-heading.-sticky {
+      top: -10px;
+    }
   }
 }
 
-body,
+#content,
 .column.-scrollable {
   &::-webkit-scrollbar {
     display: block;
@@ -99,42 +155,10 @@ body,
   }
 }
 
-.underlay {
-  background-color: rgba(0,0,0,0.15);
-  background-color: var(--underlay, rgba(0,0,0,0.15));
-}
-
 .text-center {
   text-align: center;
 }
 
-html {
-  font-size: 14px;
-}
-
-body {
-  overscroll-behavior-y: none;
-  font-family: sans-serif;
-  font-family: var(--interfaceFont, sans-serif);
-  margin: 0;
-  color: $fallback--text;
-  color: var(--text, $fallback--text);
-  max-width: 100vw;
-  overflow-x: hidden;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-
-  &.hidden {
-    display: none;
-  }
-}
-
-a {
-  text-decoration: none;
-  color: $fallback--link;
-  color: var(--link, $fallback--link);
-}
-
 .button-default {
   user-select: none;
   color: $fallback--text;
@@ -166,12 +190,12 @@ a {
   }
 
   &:hover {
-    box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
+    box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
     box-shadow: var(--buttonHoverShadow);
   }
 
   &:active {
-    box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
+    box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
     box-shadow: var(--buttonPressedShadow);
     color: $fallback--text;
     color: var(--btnPressedText, $fallback--text);
@@ -204,7 +228,7 @@ a {
     color: var(--btnToggledText, $fallback--text);
     background-color: $fallback--fg;
     background-color: var(--btnToggled, $fallback--fg);
-    box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
+    box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
     box-shadow: var(--buttonPressedShadow);
 
     svg,
@@ -254,8 +278,9 @@ a {
   }
 }
 
-input, textarea, .input {
-
+input,
+textarea,
+.input {
   &.unstyled {
     border-radius: 0;
     background: none;
@@ -266,7 +291,7 @@ input, textarea, .input {
   border: none;
   border-radius: $fallback--inputRadius;
   border-radius: var(--inputRadius, $fallback--inputRadius);
-  box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
+  box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
   box-shadow: var(--inputShadow);
   background-color: $fallback--fg;
   background-color: var(--input, $fallback--fg);
@@ -282,9 +307,11 @@ input, textarea, .input {
   height: 28px;
   line-height: 16px;
   hyphens: none;
-  padding: 8px .5em;
+  padding: 8px 0.5em;
 
-  &:disabled, &[disabled=disabled], &.disabled {
+  &:disabled,
+  &[disabled=disabled],
+  &.disabled {
     cursor: not-allowed;
     opacity: 0.5;
   }
@@ -299,18 +326,21 @@ input, textarea, .input {
 
   &[type=radio] {
     display: none;
+
     &:checked + label::before {
-      box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
-      box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
+      box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
+      box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
       background-color: var(--accent, $fallback--link);
     }
+
     &:disabled {
       &,
       & + label,
       & + label::before {
-        opacity: .5;
+        opacity: 0.5;
       }
     }
+
     + label::before {
       flex-shrink: 0;
       display: inline-block;
@@ -319,9 +349,9 @@ input, textarea, .input {
       width: 1.1em;
       height: 1.1em;
       border-radius: 100%; // Radio buttons should always be circle
-      box-shadow: 0px 0px 2px black inset;
+      box-shadow: 0 0 2px black inset;
       box-shadow: var(--inputShadow);
-      margin-right: .5em;
+      margin-right: 0.5em;
       background-color: $fallback--fg;
       background-color: var(--input, $fallback--fg);
       vertical-align: top;
@@ -337,17 +367,20 @@ input, textarea, .input {
 
   &[type=checkbox] {
     display: none;
+
     &:checked + label::before {
       color: $fallback--text;
       color: var(--inputText, $fallback--text);
     }
+
     &:disabled {
       &,
       & + label,
       & + label::before {
-        opacity: .5;
+        opacity: 0.5;
       }
     }
+
     + label::before {
       flex-shrink: 0;
       display: inline-block;
@@ -357,9 +390,9 @@ input, textarea, .input {
       height: 1.1em;
       border-radius: $fallback--checkboxRadius;
       border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
-      box-shadow: 0px 0px 2px black inset;
+      box-shadow: 0 0 2px black inset;
       box-shadow: var(--inputShadow);
-      margin-right: .5em;
+      margin-right: 0.5em;
       background-color: $fallback--fg;
       background-color: var(--input, $fallback--fg);
       vertical-align: top;
@@ -387,6 +420,7 @@ option {
 
 .hide-number-spinner {
   -moz-appearance: textfield;
+
   &[type=number]::-webkit-inner-spin-button,
   &[type=number]::-webkit-outer-spin-button {
     opacity: 0;
@@ -394,7 +428,8 @@ option {
   }
 }
 
-i[class*=icon-], .svg-inline--fa  {
+i[class*=icon-],
+.svg-inline--fa {
   color: $fallback--icon;
   color: var(--icon, $fallback--icon);
 }
@@ -426,10 +461,6 @@ i[class*=icon-], .svg-inline--fa  {
 }
 
 .auto-size {
-  flex: 1
-}
-
-main-router {
   flex: 1;
 }
 
@@ -439,23 +470,21 @@ main-router {
 
   p {
     margin: 0;
-    font-size: 0.8em
+    font-size: 0.8em;
   }
 }
 
 /* Panel */
-
 .panel {
-  display: flex;
   position: relative;
-
+  display: flex;
   flex-direction: column;
-  margin: 0.5em;
-
+  z-index: 0;
   background-color: $fallback--bg;
   background-color: var(--bg, $fallback--bg);
 
-  &::after, & {
+  &::after,
+  & {
     border-radius: $fallback--panelRadius;
     border-radius: var(--panelRadius, $fallback--panelRadius);
   }
@@ -463,16 +492,14 @@ main-router {
   &::after {
     content: '';
     position: absolute;
-
     top: 0;
     bottom: 0;
     left: 0;
     right: 0;
-
-    pointer-events: none;
-
-    box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+    z-index: 2;
+    box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
     box-shadow: var(--panelShadow);
+    pointer-events: none;
   }
 }
 
@@ -484,19 +511,67 @@ main-router {
 }
 
 .panel-heading {
+  position: relative;
+  box-sizing: border-box;
   display: flex;
   flex: none;
   border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
   border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
   background-size: cover;
-  padding: .6em .6em;
+  padding: 0.6em 0.6em;
   text-align: left;
   line-height: 28px;
   color: var(--panelText);
-  background-color: $fallback--fg;
-  background-color: var(--panel, $fallback--fg);
+  background-color: $fallback--bg;
+  background-color: var(--bg, $fallback--bg);
   align-items: baseline;
-  box-shadow: var(--panelHeaderShadow);
+  height: var(--panelHeadingHeight);
+  z-index: -2;
+
+  --panelHeadingHeight: 45px;
+
+  &.-flexible-height {
+    --panelHeadingHeight: auto;
+
+    &::after,
+    &::before {
+      display: none;
+    }
+  }
+
+  &.-sticky {
+    position: sticky;
+    top: 0;
+    z-index: 2;
+  }
+
+  &::after,
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    left: 0;
+    pointer-events: none;
+  }
+
+  &::after {
+    background-color: $fallback--fg;
+    background-color: var(--panel, $fallback--fg);
+    z-index: -2;
+    border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
+    border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
+    box-shadow: var(--panelHeaderShadow);
+  }
+
+  &::before {
+    bottom: -20px;
+    z-index: -1;
+    border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
+    border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
+    mask: linear-gradient(to bottom, white var(--panelHeadingHeight), transparent var(--panelHeadingHeight));
+  }
 
   .title {
     flex: 1 0 auto;
@@ -527,7 +602,7 @@ main-router {
     min-height: 0;
     box-sizing: border-box;
     margin: 0;
-    margin-left: .5em;
+    margin-left: 0.5em;
     min-width: 1px;
     align-self: stretch;
   }
@@ -562,7 +637,7 @@ main-router {
   a,
   .-link {
     color: $fallback--link;
-    color: var(--panelLink, $fallback--link)
+    color: var(--panelLink, $fallback--link);
   }
 }
 
@@ -574,7 +649,7 @@ main-router {
 /* TODO Should remove timeline-footer from here when we refactor panels into
  * separate component and utilize slots
  */
-.panel-footer, .timeline-footer {
+.panel-footer {
   display: flex;
   border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
   border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
@@ -605,37 +680,18 @@ main-router {
   margin: 0;
 }
 
-.container > * {
-  min-width: 0px;
-}
-
 .fa {
   color: grey;
 }
 
-nav {
-  z-index: 1000;
-  color: var(--topBarText);
-  background-color: $fallback--fg;
-  background-color: var(--topBar, $fallback--fg);
-  color: $fallback--faint;
-  color: var(--faint, $fallback--faint);
-  box-shadow: 0px 0px 4px rgba(0,0,0,.6);
-  box-shadow: var(--topBarShadow);
-  box-sizing: border-box;
-}
-
-.fade-enter-active, .fade-leave-active {
-  transition: opacity .2s
-}
-.fade-enter-from, .fade-leave-active {
-  opacity: 0
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.2s;
 }
 
-.main {
-  flex-basis: 50%;
-  flex-grow: 1;
-  flex-shrink: 1;
+.fade-enter-from,
+.fade-leave-active {
+  opacity: 0;
 }
 
 .sidebar-bounds {
@@ -653,34 +709,6 @@ nav {
   display: none;
 }
 
-@media all and (min-width: 800px) {
-  .sidebar-bounds {
-    overflow: hidden;
-    max-height: 100vh;
-    width: 345px;
-    position: fixed;
-    margin-top: -10px;
-
-    .sidebar-scroller {
-      height: 96vh;
-      width: 365px;
-      padding-top: 10px;
-      padding-right: 50px;
-      overflow-x: hidden;
-      overflow-y: scroll;
-    }
-
-    .sidebar {
-      width: 345px;
-    }
-  }
-  .sidebar-flexer {
-    max-height: 96vh;
-    flex-shrink: 0;
-    flex-grow: 0;
-  }
-}
-
 .badge {
   box-sizing: border-box;
   display: inline-block;
@@ -764,7 +792,7 @@ nav {
 }
 
 .visibility-notice {
-  padding: .5em;
+  padding: 0.5em;
   border: 1px solid $fallback--faint;
   border: 1px solid var(--faint, $fallback--faint);
   border-radius: $fallback--inputRadius;
@@ -779,7 +807,7 @@ nav {
     position: absolute;
     top: 0;
     right: 0;
-    padding: .5em;
+    padding: 0.5em;
     color: inherit;
   }
 }
@@ -796,30 +824,57 @@ nav {
   }
 }
 
-@keyframes shakeError {
-  0% {
-    transform: translateX(0);
-  }
-  15% {
-    transform: translateX(0.375rem);
-  }
-  30% {
-    transform: translateX(-0.375rem);
-  }
-  45% {
-    transform: translateX(0.375rem);
-  }
-  60% {
-    transform: translateX(-0.375rem);
+.login-hint {
+  text-align: center;
+
+  @media all and (min-width: 801px) {
+    display: none;
   }
-  75% {
-    transform: translateX(0.375rem);
+
+  a {
+    display: inline-block;
+    padding: 1em 0;
+    width: 100%;
   }
-  90% {
-    transform: translateX(-0.375rem);
+}
+
+.btn.button-default {
+  min-height: 28px;
+}
+
+.new-status-notification {
+  position: relative;
+  font-size: 1.1em;
+  z-index: 1;
+  flex: 1;
+}
+
+@media all and (min-width: 800px) {
+  .sidebar-bounds {
+    overflow: hidden;
+    max-height: 100vh;
+    width: 345px;
+    position: fixed;
+    margin-top: -10px;
+
+    .sidebar-scroller {
+      height: 96vh;
+      width: 365px;
+      padding-top: 10px;
+      padding-right: 50px;
+      overflow-x: hidden;
+      overflow-y: scroll;
+    }
+
+    .sidebar {
+      width: 345px;
+    }
   }
-  100% {
-    transform: translateX(0);
+
+  .sidebar-flexer {
+    max-height: 96vh;
+    flex-shrink: 0;
+    flex-grow: 0;
   }
 }
 
@@ -832,14 +887,6 @@ nav {
     display: flex;
   }
 
-  .container {
-    padding: 0;
-  }
-
-  .panel {
-    margin: 0.5em 0 0.5em 0;
-  }
-
   .menu-button {
     display: block;
     margin-right: 0.8em;
@@ -850,41 +897,6 @@ nav {
   }
 }
 
-.setting-list,
-.option-list{
-  list-style-type: none;
-  padding-left: 2em;
-  li {
-    margin-bottom: 0.5em;
-  }
-  .suboptions {
-    margin-top: 0.3em
-  }
-}
-
-.login-hint {
-  text-align: center;
-
-  @media all and (min-width: 801px) {
-    display: none;
-  }
-
-  a {
-    display: inline-block;
-    padding: 1em 0px;
-    width: 100%;
-  }
-}
-
-.btn.button-default {
-  min-height: 28px;
-}
-
-.animate-spin {
-  animation: spin 2s infinite linear;
-  display: inline-block;
-}
-
 @keyframes spin {
   0% {
     transform: rotate(0deg);
@@ -895,45 +907,36 @@ nav {
   }
 }
 
-.new-status-notification {
-  position: relative;
-  font-size: 1.1em;
-  z-index: 1;
-  flex: 1;
-}
+@keyframes shakeError {
+  0% {
+    transform: translateX(0);
+  }
 
-.chat-layout {
-  // Needed for smoother chat navigation in the desktop Safari (otherwise the chat layout "jumps" as the chat opens).
-  overflow: hidden;
-  height: 100%;
+  15% {
+    transform: translateX(0.375rem);
+  }
 
-  // Get rid of scrollbar on body as scrolling happens on different element
-  // Ensures the fixed position of the mobile browser bars on scroll up / down events.
-  // Prevents the mobile browser bars from overlapping or hiding the message posting form.
-  @media all and (max-width: 800px) {
-    body {
-      height: 100%;
-    }
+  30% {
+    transform: translateX(-0.375rem);
+  }
 
-    #app {
-      height: 100%;
-      overflow: hidden;
-      min-height: auto;
-    }
+  45% {
+    transform: translateX(0.375rem);
+  }
 
-    #app_bg_wrapper {
-      overflow: hidden;
-    }
+  60% {
+    transform: translateX(-0.375rem);
+  }
 
-    .main {
-      overflow: hidden;
-      height: 100%;
-    }
+  75% {
+    transform: translateX(0.375rem);
+  }
 
-    #content {
-      padding-top: 0;
-      height: 100%;
-      overflow: visible;
-    }
+  90% {
+    transform: translateX(-0.375rem);
+  }
+
+  100% {
+    transform: translateX(0);
   }
 }
diff --git a/src/App.vue b/src/App.vue
index dc2359f3..71de2a36 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -7,28 +7,28 @@
       id="app_bg_wrapper"
       class="app-bg-wrapper"
     />
-    <MobileNav class="navbar" v-if="isMobileLayout" />
-    <DesktopNav class="navbar" v-else />
-    <div class="app-bg-wrapper app-container-wrapper" />
+    <MobileNav v-if="isMobileLayout" />
+    <DesktopNav v-else />
     <div
       id="content"
       class="app-layout container"
+      :class="{ '-reverse': reverseLayout }"
     >
       <div class="underlay"/>
       <div
+        id="sidebar"
         class="column -scrollable -mini mobile-hidden"
-        :style="sidebarAlign"
       >
         <user-panel />
-        <div v-if="!isMobileLayout">
+        <template v-if="!isMobileLayout">
           <nav-panel />
           <instance-specific-panel v-if="showInstanceSpecificPanel" />
           <features-panel v-if="!currentUser && showFeaturesPanel" />
           <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
           <notifications v-if="currentUser" />
-        </div>
+        </template>
       </div>
-      <div class="column main">
+      <div id="main-scroller" class="column main">
         <div
           v-if="!currentUser"
           class="login-hint panel panel-default"
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index aef11712..59e884c4 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -6,7 +6,7 @@ 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 { promiseInterval } from '../../services/promise_interval/promise_interval.js'
-import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
+import { getScrollPosition, getNewTopPosition, isBottomedOut } from './chat_layout_utils.js'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faChevronDown,
@@ -20,7 +20,7 @@ library.add(
 )
 
 const BOTTOMED_OUT_OFFSET = 10
-const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
+const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
 const SAFE_RESIZE_TIME_OFFSET = 100
 const MARK_AS_READ_DELAY = 1500
 const MAX_RETRIES = 10
@@ -52,7 +52,6 @@ const Chat = {
     }
 
     this.$nextTick(() => {
-      this.updateScrollableContainerHeight()
       this.handleResize()
     })
     this.setChatLayout()
@@ -132,7 +131,6 @@ const Chat = {
     onFilesDropped () {
       this.$nextTick(() => {
         this.handleResize()
-        this.updateScrollableContainerHeight()
       })
     },
     handleVisibilityChange () {
@@ -154,10 +152,6 @@ const Chat = {
       if (html) {
         html.classList.add('chat-layout')
       }
-
-      this.$nextTick(() => {
-        this.updateScrollableContainerHeight()
-      })
     },
     unsetChatLayout () {
       let html = document.querySelector('html')
@@ -167,17 +161,9 @@ const Chat = {
     },
     handleLayoutChange () {
       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 = {}) {
       const { expand = false, delayed = false } = opts
@@ -190,17 +176,14 @@ const Chat = {
       }
 
       this.$nextTick(() => {
-        this.updateScrollableContainerHeight()
-
         const { offsetHeight = undefined } = this.lastScrollPosition
-        this.lastScrollPosition = getScrollPosition(this.$refs.scrollable)
+        this.lastScrollPosition = getScrollPosition(document.getElementById('content'))
 
         const diff = this.lastScrollPosition.offsetHeight - offsetHeight
         if (diff < 0 || (!this.bottomedOut() && expand)) {
           this.$nextTick(() => {
-            this.updateScrollableContainerHeight()
-            this.$refs.scrollable.scrollTo({
-              top: this.$refs.scrollable.scrollTop - diff,
+            document.getElementById('content').scrollTo({
+              top: document.getElementById('content').scrollTop - diff,
               left: 0
             })
           })
@@ -209,7 +192,7 @@ const Chat = {
     },
     scrollDown (options = {}) {
       const { behavior = 'auto', forceRead = false } = options
-      const scrollable = this.$refs.scrollable
+      const scrollable = document.getElementById('content')
       if (!scrollable) { return }
       this.$nextTick(() => {
         scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
@@ -228,10 +211,10 @@ const Chat = {
       })
     },
     bottomedOut (offset) {
-      return isBottomedOut(this.$refs.scrollable, offset)
+      return isBottomedOut(document.getElementById('content'), offset)
     },
     reachedTop () {
-      const scrollable = this.$refs.scrollable
+      const scrollable = document.getElementById('content')
       return scrollable && scrollable.scrollTop <= 0
     },
     cullOlderCheck () {
@@ -263,7 +246,7 @@ const Chat = {
       }
     }, 200),
     handleScrollUp (positionBeforeLoading) {
-      const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
+      const positionAfterLoading = getScrollPosition(document.getElementById('content'))
       this.$refs.scrollable.scrollTo({
         top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
         left: 0
@@ -285,22 +268,18 @@ const Chat = {
             chatService.clear(chatMessageService)
           }
 
-          const positionBeforeUpdate = getScrollPosition(this.$refs.scrollable)
+          const positionBeforeUpdate = getScrollPosition(document.getElementById('content'))
           this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
             this.$nextTick(() => {
               if (fetchOlderMessages) {
                 this.handleScrollUp(positionBeforeUpdate)
               }
 
-              if (isFirstFetch) {
-                this.updateScrollableContainerHeight()
-              }
-
               // In vertical screens, the first batch of fetched messages may not always take the
               // full height of the scrollable container.
               // If this is the case, we want to fetch the messages until the scrollable container
               // is fully populated so that the user has the ability to scroll up and load the history.
-              if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
+              if (messages.length > 0) {
                 this.fetchChat({ maxId: this.currentChatMessageService.minId })
               }
             })
@@ -336,9 +315,6 @@ const Chat = {
         this.handleResize()
         // When the posting form size changes because of a media attachment, we need an extra resize
         // to account for the potential delay in the DOM update.
-        setTimeout(() => {
-          this.updateScrollableContainerHeight()
-        }, SAFE_RESIZE_TIME_OFFSET)
         this.scrollDown({ forceRead: true })
       })
     },
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 3a26686c..c1f2dcf2 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -1,19 +1,12 @@
 .chat-view {
   display: flex;
-  height: calc(100vh - 60px);
-  width: 100%;
-
-  .chat-title {
-    // prevents chat header jumping on when the user avatar loads
-    height: 28px;
-  }
+  height: 100%;
 
   .chat-view-inner {
     height: auto;
     width: 100%;
     overflow: visible;
     display: flex;
-    margin: 0.5em 0.5em 0 0.5em;
   }
 
   .chat-view-body {
@@ -32,11 +25,9 @@
     }
   }
 
-  .scrollable-message-list {
+  .message-list {
     padding: 0 0.8em;
     height: 100%;
-    overflow-y: scroll;
-    overflow-x: hidden;
     display: flex;
     flex-direction: column;
   }
@@ -44,12 +35,13 @@
   .footer {
     position: sticky;
     bottom: 0;
+    background-color: $fallback--bg;
+    background-color: var(--bg, $fallback--bg);
   }
 
   .chat-view-heading {
     align-items: center;
     justify-content: space-between;
-    top: 50px;
     display: flex;
     z-index: 2;
     position: sticky;
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index 493c5d5a..3b0129fa 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -8,7 +8,7 @@
       >
         <div
           ref="header"
-          class="panel-heading chat-view-heading mobile-hidden"
+          class="panel-heading -sticky chat-view-heading mobile-hidden"
         >
           <a
             class="go-back-button"
@@ -27,10 +27,8 @@
           </div>
         </div>
         <div
-          ref="scrollable"
-          class="scrollable-message-list"
+          class="message-list"
           :style="{ height: scrollableContainerHeight }"
-          @scroll="handleScroll"
         >
           <template v-if="!errorLoadingChat">
             <ChatMessage
diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js
index 50a933ac..5c4aa5bf 100644
--- a/src/components/chat/chat_layout_utils.js
+++ b/src/components/chat/chat_layout_utils.js
@@ -1,5 +1,6 @@
 // Captures a scroll position
 export const getScrollPosition = (el) => {
+  console.log(el)
   return {
     scrollTop: el.scrollTop,
     scrollHeight: el.scrollHeight,
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 229f3006..1c28ef77 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -181,7 +181,7 @@
   .mobile-notifications {
     margin-top: 50px;
     width: 100vw;
-    height: calc(100vh - 50px);
+    height: calc(100vh - var(--navbar-height));
     overflow-x: hidden;
     overflow-y: scroll;
 
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 2ce5d56f..9d8fa059 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -6,7 +6,7 @@
     <div :class="mainClass">
       <div
         v-if="!noHeading"
-        class="panel-heading"
+        class="notifications-heading panel-heading -sticky"
       >
         <div class="title">
           {{ $t('notifications.notifications') }}
diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue
index 3ffa5425..e0579568 100644
--- a/src/components/password_reset/password_reset.vue
+++ b/src/components/password_reset/password_reset.vue
@@ -91,6 +91,9 @@
     flex-direction: column;
     margin-top: 0.6em;
     max-width: 18rem;
+    > * {
+      min-width: 0;
+    }
   }
 
   .form-group {
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 3d409109..1e735c69 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -271,7 +271,10 @@ $validations-cRed: #f04124;
   .container {
     display: flex;
     flex-direction: row;
-    //margin-bottom: 1em;
+
+    > * {
+      min-width: 0;
+    }
   }
 
   .terms-of-service {
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 2f7649a9..f3738a8c 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -2,6 +2,18 @@
 .settings-modal {
   overflow: hidden;
 
+  .setting-list,
+  .option-list {
+    list-style-type: none;
+    padding-left: 2em;
+    li {
+      margin-bottom: 0.5em;
+    }
+    .suboptions {
+      margin-top: 0.3em
+    }
+  }
+
   &.peek {
     .settings-modal-panel {
       /* Explanation:
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 5ed43e0d..e38e2a53 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -42,6 +42,10 @@
     display: flex;
     padding: var(--status-margin, $status-margin);
 
+    > * {
+      min-width: 0;
+    }
+
     &.-repeat {
       padding-top: 0;
     }
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 8ec5d1e5..9056319b 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -64,7 +64,7 @@ const Timeline = {
       if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
       return {
         root: rootClasses,
-        header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
+        header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []),
         body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
         footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
       }
@@ -89,7 +89,7 @@ const Timeline = {
     const credentials = store.state.users.currentUser.credentials
     const showImmediately = this.timeline.visibleStatuses.length === 0
 
-    window.addEventListener('scroll', this.handleScroll)
+    document.getElementById('content').addEventListener('scroll', this.handleScroll)
 
     if (store.state.api.fetchers[this.timelineName]) { return false }
 
@@ -111,7 +111,7 @@ const Timeline = {
     setTimeout(this.determineVisibleStatuses, 250)
   },
   unmounted () {
-    window.removeEventListener('scroll', this.handleScroll)
+    document.getElementById('content').removeEventListener('scroll', this.handleScroll)
     window.removeEventListener('keydown', this.handleShortKey)
     if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
     this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss
index 2c5a67e2..c10375d4 100644
--- a/src/components/timeline/timeline.scss
+++ b/src/components/timeline/timeline.scss
@@ -13,7 +13,6 @@
     max-width: 100%;
     flex-wrap: nowrap;
     align-items: center;
-    position: relative;
 
     .loadmore-button {
       flex-shrink: 0;
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 14b4643a..6e044bb6 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -8,7 +8,7 @@
       :style="style"
       class="background-image"
     />
-    <div class="panel-heading">
+    <div class="panel-heading -flexible-height">
       <div class="user-info">
         <div class="container">
           <a
@@ -331,6 +331,7 @@
     border-top-left-radius: calc(var(--panelRadius) - 1px);
     border-top-right-radius: calc(var(--panelRadius) - 1px);
     background-color: var(--profileBg);
+    z-index: -2;
 
     &.hide-bio {
       mask-size: 100% 40px;
@@ -385,11 +386,16 @@
   padding: 0 26px;
 
   .container {
+    min-width: 0;
     padding: 16px 0 6px;
     display: flex;
     align-items: flex-start;
     max-height: 56px;
 
+    > * {
+      min-width: 0;
+    }
+
     .Avatar {
       --_avatarShadowBox: var(--avatarShadow);
       --_avatarShadowFilter: var(--avatarShadowFilter);
-- 
cgit v1.2.3-70-g09d2


From 4a068483ed9b1334780402cbe64dfa3f4a0e8a3a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 5 Apr 2022 18:38:05 +0300
Subject: wide mode initial implementation + cleanup

---
 src/App.js                               |   8 +-
 src/App.scss                             | 127 +++++++++++++++++--------------
 src/App.vue                              |   6 +-
 src/boot/after_store.js                  |   7 +-
 src/components/about/about.vue           |   2 +-
 src/components/mobile_nav/mobile_nav.vue |   1 -
 src/components/user_card/user_card.vue   |   1 +
 src/modules/interface.js                 |  10 +--
 8 files changed, 89 insertions(+), 73 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index 4a59f412..b2ec95d4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -79,7 +79,7 @@ export default {
     hideShoutbox () {
       return this.$store.getters.mergedConfig.hideShoutbox
     },
-    isMobileLayout () { return this.$store.state.interface.mobileLayout },
+    layoutType () { return this.$store.state.interface.layoutType },
     privateMode () { return this.$store.state.instance.private },
     reverseLayout () { return this.$store.getters.mergedConfig.sidebarRight },
     ...mapGetters(['mergedConfig'])
@@ -87,10 +87,12 @@ export default {
   methods: {
     updateMobileState () {
       const mobileLayout = windowWidth() <= 800
+      const wideLayout = windowWidth() >= 1300
       const layoutHeight = windowHeight()
-      const changed = mobileLayout !== this.isMobileLayout
+      const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
+      const changed = layoutType !== this.layoutType
       if (changed) {
-        this.$store.dispatch('setMobileLayout', mobileLayout)
+        this.$store.dispatch('setLayoutType', layoutType)
       }
       this.$store.dispatch('setLayoutHeight', layoutHeight)
     }
diff --git a/src/App.scss b/src/App.scss
index c8a7969c..e32a2d36 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -63,11 +63,11 @@ nav {
 }
 
 #sidebar {
-  display: grid;
-  grid-template-columns: 100%;
-  row-gap: 1em;
   grid-area: sidebar;
-  align-content: start;
+
+  @media all and (max-width: 800px) {
+    display: none;
+  }
 }
 
 #main-scroller {
@@ -88,6 +88,19 @@ nav {
   background-position: 50%;
 }
 
+.underlay {
+  grid-column-start: 1;
+  grid-column-end: span 3;
+  grid-row-start: 1;
+  grid-row-end: 1;
+  margin: 0 -0.5em;
+  padding: 0 0.5em;
+  pointer-events: none;
+  background-color: rgba(0, 0, 0, 0.15);
+  background-color: var(--underlay, rgba(0, 0, 0, 0.15));
+  z-index: -2000;
+}
+
 .app-layout {
   position: relative;
   display: grid;
@@ -103,44 +116,66 @@ nav {
   justify-content: center;
 
   --miniColumn: 345px;
-  --maxiColumn: minmax(auto, 615px);
+  --maxiColumn: minmax(345px, 615px);
+
+  .column {
+    display: grid;
+    grid-template-columns: 100%;
+    box-sizing: border-box;
+    padding-top: 10px;
+    grid-row-start: 1;
+    grid-row-end: 1;
+    margin: 0 0.5em;
+    row-gap: 1em;
+    align-content: start;
+
+    &.-scrollable {
+      padding-top: 10px;
+      position: sticky;
+      top: 0;
+      max-height: calc(100vh - var(--navbar-height));
+      overflow-y: auto;
+      overflow-x: hidden;
 
-  &.-reverse {
+      .panel-heading.-sticky {
+        top: -10px;
+      }
+    }
+  }
+
+  .column-inner {
+    display: grid;
+    grid-template-columns: 100%;
+    box-sizing: border-box;
+    row-gap: 1em;
+    align-content: start;
+  }
+
+  &.-reverse:not(.-wide):not(.-mobile) {
     grid-template-columns: var(--maxiColumn) var(--miniColumn);
     grid-template-areas: "content sidebar";
   }
-}
 
-.underlay {
-  grid-column-start: 1;
-  grid-column-end: span 2;
-  grid-row-start: 1;
-  grid-row-end: 1;
-  margin: 0 -0.5em;
-  padding: 0 0.5em;
-  pointer-events: none;
-  background-color: rgba(0, 0, 0, 0.15);
-  background-color: var(--underlay, rgba(0, 0, 0, 0.15));
-  z-index: -2000;
-}
+  &.-wide {
+    grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
+    grid-template-areas: "sidebar content notifs";
 
-.column {
-  box-sizing: border-box;
-  padding-top: 10px;
-  grid-row-start: 1;
-  grid-row-end: 1;
-  margin: 0 0.5em;
+    &.-reverse {
+      grid-template-areas: "notifs content sidebar";
+    }
+  }
 
-  &.-scrollable {
-    padding-top: 10px;
-    position: sticky;
-    top: 0;
-    max-height: calc(100vh - var(--navbar-height));
-    overflow-y: auto;
-    overflow-x: hidden;
+  &.-mobile {
+    grid-template-columns: 100vw;
+    grid-template-areas: "content";
+    padding: 0;
+
+    .column {
+      margin: 0;
+    }
 
-    .panel-heading.-sticky {
-      top: -10px;
+    .underlay {
+      display: none;
     }
   }
 }
@@ -464,17 +499,6 @@ i[class*=icon-],
   color: grey;
 }
 
-.sidebar-bounds {
-  flex: 0;
-  flex-basis: 35%;
-}
-
-.sidebar-flexer {
-  flex: 1;
-  flex-basis: 345px;
-  width: 365px;
-}
-
 .mobile-shown {
   display: none;
 }
@@ -652,19 +676,6 @@ i[class*=icon-],
   .mobile-hidden {
     display: none;
   }
-
-  .panel-switcher {
-    display: flex;
-  }
-
-  .menu-button {
-    display: block;
-    margin-right: 0.8em;
-  }
-
-  .main {
-    margin-bottom: 7em;
-  }
 }
 
 @keyframes spin {
diff --git a/src/App.vue b/src/App.vue
index 71de2a36..f87d895b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -7,12 +7,12 @@
       id="app_bg_wrapper"
       class="app-bg-wrapper"
     />
-    <MobileNav v-if="isMobileLayout" />
+    <MobileNav v-if="layoutType === 'mobile'" />
     <DesktopNav v-else />
     <div
       id="content"
       class="app-layout container"
-      :class="{ '-reverse': reverseLayout }"
+      :class="[{ '-reverse': reverseLayout }, '-' + layoutType]"
     >
       <div class="underlay"/>
       <div
@@ -20,7 +20,7 @@
         class="column -scrollable -mini mobile-hidden"
       >
         <user-panel />
-        <template v-if="!isMobileLayout">
+        <template v-if="layoutType !== 'mobile'">
           <nav-panel />
           <instance-specific-panel v-if="showInstanceSpecificPanel" />
           <features-panel v-if="!currentUser && showFeaturesPanel" />
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 76832708..ce96141d 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -332,8 +332,11 @@ const checkOAuthToken = async ({ store }) => {
 }
 
 const afterStoreSetup = async ({ store, i18n }) => {
-  const width = windowWidth()
-  store.dispatch('setMobileLayout', width <= 800)
+  // TODO cleanup copypasta
+  const mobileLayout = windowWidth() <= 800
+  const wideLayout = windowWidth() >= 1300
+  const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
+  store.dispatch('setLayoutType', layoutType)
 
   FaviconService.initFaviconService()
 
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index 518f6184..5d5d6479 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="sidebar">
+  <div class="column-inner">
     <instance-specific-panel v-if="showInstanceSpecificPanel" />
     <staff-panel />
     <terms-of-service-panel />
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 1c28ef77..09680910 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -96,7 +96,6 @@
     grid-template-rows: 50px;
     grid-template-columns: 2fr auto;
     width: 100%;
-    position: fixed;
     box-sizing: border-box;
   }
 
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 6e044bb6..43a660e1 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -289,6 +289,7 @@
 
 .user-card {
   position: relative;
+  z-index: 1;
 
   &:hover {
     --_still-image-img-visibility: visible;
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 17277331..9df72b88 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -13,7 +13,7 @@ const defaultState = {
       window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
     )
   },
-  mobileLayout: false,
+  layoutType: 'normal',
   globalNotices: [],
   layoutHeight: 0,
   lastTimeline: null
@@ -36,8 +36,8 @@ const interfaceMod = {
     setNotificationPermission (state, permission) {
       state.notificationPermission = permission
     },
-    setMobileLayout (state, value) {
-      state.mobileLayout = value
+    setLayoutType (state, value) {
+      state.layoutType = value
     },
     closeSettingsModal (state) {
       state.settingsModalState = 'hidden'
@@ -86,8 +86,8 @@ const interfaceMod = {
     setNotificationPermission ({ commit }, permission) {
       commit('setNotificationPermission', permission)
     },
-    setMobileLayout ({ commit }, value) {
-      commit('setMobileLayout', value)
+    setLayoutType ({ commit }, value) {
+      commit('setLayoutType', value)
     },
     closeSettingsModal ({ commit }) {
       commit('closeSettingsModal')
-- 
cgit v1.2.3-70-g09d2


From 6a319154d94c51f0464e0de31c6f88fbe20defea Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 5 Apr 2022 19:22:15 +0300
Subject: teleport bread

---
 src/App.js                                     |   4 +-
 src/App.scss                                   |  10 ++-
 src/App.vue                                    |   9 +-
 src/components/mobile_nav/mobile_nav.js        |   3 +-
 src/components/mobile_nav/mobile_nav.vue       |   5 +-
 src/components/notifications/notifications.js  |  15 +++-
 src/components/notifications/notifications.vue | 116 +++++++++++++------------
 7 files changed, 88 insertions(+), 74 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index b2ec95d4..827692a6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,5 @@
 import UserPanel from './components/user_panel/user_panel.vue'
 import NavPanel from './components/nav_panel/nav_panel.vue'
-import Notifications from './components/notifications/notifications.vue'
 import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
 import FeaturesPanel from './components/features_panel/features_panel.vue'
 import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
@@ -16,13 +15,14 @@ import PostStatusModal from './components/post_status_modal/post_status_modal.vu
 import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
 import { windowWidth, windowHeight } from './services/window_utils/window_utils'
 import { mapGetters } from 'vuex'
+import { defineAsyncComponent } from 'vue'
 
 export default {
   name: 'app',
   components: {
     UserPanel,
     NavPanel,
-    Notifications,
+    Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
     InstanceSpecificPanel,
     FeaturesPanel,
     WhoToFollowPanel,
diff --git a/src/App.scss b/src/App.scss
index e32a2d36..1867a54d 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -64,10 +64,10 @@ nav {
 
 #sidebar {
   grid-area: sidebar;
+}
 
-  @media all and (max-width: 800px) {
-    display: none;
-  }
+#notifs-column {
+  grid-area: notifs;
 }
 
 #main-scroller {
@@ -177,6 +177,10 @@ nav {
     .underlay {
       display: none;
     }
+
+    #sidebar {
+      display: none;
+    }
   }
 }
 
diff --git a/src/App.vue b/src/App.vue
index f87d895b..70084538 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -9,23 +9,21 @@
     />
     <MobileNav v-if="layoutType === 'mobile'" />
     <DesktopNav v-else />
+    <notifications v-if="currentUser" />
     <div
       id="content"
       class="app-layout container"
       :class="[{ '-reverse': reverseLayout }, '-' + layoutType]"
     >
       <div class="underlay"/>
-      <div
-        id="sidebar"
-        class="column -scrollable -mini mobile-hidden"
-      >
+      <div id="sidebar" class="column -scrollable">
         <user-panel />
         <template v-if="layoutType !== 'mobile'">
           <nav-panel />
           <instance-specific-panel v-if="showInstanceSpecificPanel" />
           <features-panel v-if="!currentUser && showFeaturesPanel" />
           <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
-          <notifications v-if="currentUser" />
+          <div id="notifs-sidebar" />
         </template>
       </div>
       <div id="main-scroller" class="column main">
@@ -42,6 +40,7 @@
         </div>
         <router-view />
       </div>
+      <div id="notifs-column" class="column -scrollable"/>
       <media-modal />
     </div>
     <shout-panel
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index 9e736cfb..877d52a9 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -78,7 +78,8 @@ const MobileNav = {
       this.$store.dispatch('logout')
     },
     markNotificationsAsSeen () {
-      this.$refs.notifications.markAsSeen()
+      // this.$refs.notifications.markAsSeen()
+      this.$store.dispatch('markNotificationsAsSeen')
     },
     onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
       if (scrollTop + clientHeight >= scrollHeight) {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 09680910..6c810bba 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -69,12 +69,9 @@
       </div>
       <div
         class="mobile-notifications"
+        id="mobile-notifications"
         @scroll="onScroll"
       >
-        <Notifications
-          ref="notifications"
-          :no-heading="true"
-        />
       </div>
     </div>
     <SideDrawer
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index c8f1ebcb..2bb8a1d1 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -23,8 +23,6 @@ const Notifications = {
     NotificationFilters
   },
   props: {
-    // Disables display of panel header
-    noHeading: Boolean,
     // Disables panel styles, unread mark, potentially other notification-related actions
     // meant for "Interactions" timeline
     minimalMode: Boolean,
@@ -65,6 +63,19 @@ const Notifications = {
     loading () {
       return this.$store.state.statuses.notifications.loading
     },
+    noHeading () {
+      const { layoutType } = this.$store.state.interface
+      console.log(layoutType)
+      return layoutType === 'mobile'
+    },
+    teleportTarget () {
+      const { layoutType } = this.$store.state.interface
+      const map = {
+        wide: '#notifs-column',
+        mobile: '#mobile-notifications'
+      }
+      return map[layoutType] || '#notifs-sidebar'
+    },
     notificationsToDisplay () {
       return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
     },
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 9d8fa059..f50d3d38 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -1,69 +1,71 @@
 <template>
-  <div
-    :class="{ minimal: minimalMode }"
-    class="Notifications"
-  >
-    <div :class="mainClass">
-      <div
-        v-if="!noHeading"
-        class="notifications-heading panel-heading -sticky"
-      >
-        <div class="title">
-          {{ $t('notifications.notifications') }}
-          <span
-            v-if="unseenCount"
-            class="badge badge-notification unseen-count"
-          >{{ unseenCount }}</span>
-        </div>
-        <button
-          v-if="unseenCount"
-          class="button-default read-button"
-          @click.prevent="markAsSeen"
-        >
-          {{ $t('notifications.read') }}
-        </button>
-        <NotificationFilters />
-      </div>
-      <div class="panel-body">
+  <teleport :to="teleportTarget">
+    <div
+      :class="{ minimal: minimalMode }"
+      class="Notifications"
+    >
+      <div :class="mainClass">
         <div
-          v-for="notification in notificationsToDisplay"
-          :key="notification.id"
-          class="notification"
-          :class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
+          v-if="!noHeading"
+          class="notifications-heading panel-heading -sticky"
         >
-          <div class="notification-overlay" />
-          <notification :notification="notification" />
+          <div class="title">
+            {{ $t('notifications.notifications') }}
+            <span
+              v-if="unseenCount"
+              class="badge badge-notification unseen-count"
+            >{{ unseenCount }}</span>
+          </div>
+          <button
+            v-if="unseenCount"
+            class="button-default read-button"
+            @click.prevent="markAsSeen"
+          >
+            {{ $t('notifications.read') }}
+          </button>
+          <NotificationFilters />
         </div>
-      </div>
-      <div class="panel-footer notifications-footer">
-        <div
-          v-if="bottomedOut"
-          class="new-status-notification text-center faint"
-        >
-          {{ $t('notifications.no_more_notifications') }}
+        <div class="panel-body">
+          <div
+            v-for="notification in notificationsToDisplay"
+            :key="notification.id"
+            class="notification"
+            :class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
+          >
+            <div class="notification-overlay" />
+            <notification :notification="notification" />
+          </div>
         </div>
-        <button
-          v-else-if="!loading"
-          class="button-unstyled -link -fullwidth"
-          @click.prevent="fetchOlderNotifications()"
-        >
-          <div class="new-status-notification text-center">
-            {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
+        <div class="panel-footer notifications-footer">
+          <div
+            v-if="bottomedOut"
+            class="new-status-notification text-center faint"
+          >
+            {{ $t('notifications.no_more_notifications') }}
+          </div>
+          <button
+            v-else-if="!loading"
+            class="button-unstyled -link -fullwidth"
+            @click.prevent="fetchOlderNotifications()"
+          >
+            <div class="new-status-notification text-center">
+              {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
+            </div>
+          </button>
+          <div
+            v-else
+            class="new-status-notification text-center"
+          >
+            <FAIcon
+              icon="circle-notch"
+              spin
+              size="lg"
+            />
           </div>
-        </button>
-        <div
-          v-else
-          class="new-status-notification text-center"
-        >
-          <FAIcon
-            icon="circle-notch"
-            spin
-            size="lg"
-          />
         </div>
       </div>
     </div>
-  </div>
+  </teleport>
 </template>
 
 <script src="./notifications.js"></script>
-- 
cgit v1.2.3-70-g09d2


From b5ded67c0608724fe23be15e346dd6842819635f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 7 Apr 2022 15:11:23 +0300
Subject: options to enable scrollbars and disable sticky headers

---
 src/App.js                                         |  2 ++
 src/App.scss                                       | 27 ++++++++++++++++++----
 src/App.vue                                        |  6 ++---
 src/components/settings_modal/tabs/general_tab.vue | 10 ++++++++
 src/components/timeline/timeline.js                |  2 +-
 src/modules/config.js                              |  2 ++
 src/panel.scss                                     |  3 +--
 7 files changed, 42 insertions(+), 10 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index 26a188f4..75fa3a3c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -82,6 +82,8 @@ export default {
     layoutType () { return this.$store.state.interface.layoutType },
     privateMode () { return this.$store.state.instance.private },
     reverseLayout () { return this.$store.getters.mergedConfig.sidebarRight },
+    noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
+    showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
     ...mapGetters(['mergedConfig'])
   },
   methods: {
diff --git a/src/App.scss b/src/App.scss
index 1867a54d..ac6cf3f4 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -59,6 +59,7 @@ nav {
 #content {
   overscroll-behavior-y: none;
   overflow-y: auto;
+  overflow-x: hidden;
   position: sticky;
 }
 
@@ -136,6 +137,19 @@ nav {
       max-height: calc(100vh - var(--navbar-height));
       overflow-y: auto;
       overflow-x: hidden;
+      margin-left: -2em;
+      padding-left: 2.5em;
+
+      &:not(.-show-scrollbar) {
+        scrollbar-width: none;
+        margin-right: -2em;
+        padding-right: 2.5em;
+
+        &::-webkit-scrollbar {
+          display: block;
+          width: 0;
+        }
+      }
 
       .panel-heading.-sticky {
         top: -10px;
@@ -143,6 +157,15 @@ nav {
     }
   }
 
+  &.-no-sticky-headers {
+    .column {
+      .panel-heading.-sticky {
+        position: relative;
+        top: 0;
+      }
+    }
+  }
+
   .column-inner {
     display: grid;
     grid-template-columns: 100%;
@@ -186,10 +209,6 @@ nav {
 
 #content,
 .column.-scrollable {
-  &::-webkit-scrollbar {
-    display: block;
-    width: 0;
-  }
 }
 
 .text-center {
diff --git a/src/App.vue b/src/App.vue
index 70084538..243a3314 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -13,10 +13,10 @@
     <div
       id="content"
       class="app-layout container"
-      :class="[{ '-reverse': reverseLayout }, '-' + layoutType]"
+      :class="[{ '-reverse': reverseLayout, '-no-sticky-headers': noSticky }, '-' + layoutType]"
     >
       <div class="underlay"/>
-      <div id="sidebar" class="column -scrollable">
+      <div id="sidebar" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }">
         <user-panel />
         <template v-if="layoutType !== 'mobile'">
           <nav-panel />
@@ -40,7 +40,7 @@
         </div>
         <router-view />
       </div>
-      <div id="notifs-column" class="column -scrollable"/>
+      <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
       <media-modal />
     </div>
     <shout-panel
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index a2c6bffa..e29bc7cf 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -60,6 +60,16 @@
             {{ $t('settings.virtual_scrolling') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="disableStickyHeaders">
+            {{ $t('settings.disable_sticky_headers') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="showScrollbars">
+            {{ $t('settings.show_scrollbars') }}
+          </BooleanSetting>
+        </li>
         <li>
           <BooleanSetting
             path="alwaysShowNewPostButton"
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 4b2c1e81..bfee2382 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -91,7 +91,7 @@ const Timeline = {
     const credentials = store.state.users.currentUser.credentials
     const showImmediately = this.timeline.visibleStatuses.length === 0
 
-    scroller().addEventListener('scroll', this.handleScroll)
+    scroller() && scroller().addEventListener('scroll', this.handleScroll)
 
     if (store.state.api.fetchers[this.timelineName]) { return false }
 
diff --git a/src/modules/config.js b/src/modules/config.js
index ff5ef270..6d2e6ce0 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -73,6 +73,8 @@ export const defaultState = {
   playVideosInModal: false,
   useOneClickNsfw: false,
   useContainFit: true,
+  disableStickyHeaders: false,
+  showScrollbars: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
   mentionLinkDisplay: undefined, // instance default
diff --git a/src/panel.scss b/src/panel.scss
index bb7af43a..bd56858f 100644
--- a/src/panel.scss
+++ b/src/panel.scss
@@ -28,8 +28,7 @@
 
 .panel-body:empty::before {
   content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
-  display: block;
-  margin: 1em;
+  display: block; margin: 1em;
   text-align: center;
 }
 
-- 
cgit v1.2.3-70-g09d2


From 6109fab14e4dac3a85ffa51ddfcd7e3aaef5149a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 8 Apr 2022 10:17:23 +0300
Subject: cleanup, fix some things, try to disable scrollbars on mobile

---
 src/App.scss                               | 28 ++++++++++++++++------------
 src/App.vue                                |  2 +-
 src/components/chat/chat.scss              |  9 ---------
 src/components/media_modal/media_modal.vue |  2 +-
 src/components/modal/modal.vue             |  2 +-
 5 files changed, 19 insertions(+), 24 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.scss b/src/App.scss
index dd3d8cbb..0deac2b1 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -116,7 +116,7 @@ nav {
 
 #content {
   overscroll-behavior-y: none;
-  overflow-y: auto;
+  overflow-y: scroll;
   overflow-x: hidden;
   position: sticky;
 }
@@ -198,19 +198,23 @@ nav {
       margin-left: -2em;
       padding-left: 2.5em;
 
-      &:not(.-show-scrollbar) {
-        scrollbar-width: none;
-        margin-right: -2em;
-        padding-right: 2.5em;
-
-        &::-webkit-scrollbar {
-          display: block;
-          width: 0;
+      // Only show custom scrollbars on devices which
+      // have a cursor/pointer to operate them
+      @media (pointer: fine) {
+        &:not(.-show-scrollbar) {
+          scrollbar-width: none;
+          margin-right: -2em;
+          padding-right: 2.5em;
+
+          &::-webkit-scrollbar {
+            display: block;
+            width: 0;
+          }
         }
-      }
 
-      .panel-heading.-sticky {
-        top: -10px;
+        .panel-heading.-sticky {
+          top: -10px;
+        }
       }
     }
   }
diff --git a/src/App.vue b/src/App.vue
index 243a3314..b5f6e7d3 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -41,8 +41,8 @@
         <router-view />
       </div>
       <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
-      <media-modal />
     </div>
+    <media-modal />
     <shout-panel
       v-if="currentUser && shout && !hideShoutbox"
       :floating="true"
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 51bb3ea6..85e9f59d 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -133,18 +133,9 @@
     .chat-view-heading {
       box-sizing: border-box;
       position: static;
-      z-index: 9999;
       top: 0;
       margin-top: 0;
       border-radius: 0;
-
-      /* This practically overlays the panel heading color over panel background
-       * color. This is needed because we allow transparent panel background and
-       * it doesn't work well in this "disjointed panel header" case
-       */
-      background:
-        linear-gradient(to top, var(--panel), var(--panel)),
-        linear-gradient(to top, var(--bg), var(--bg));
       height: 50px;
     }
 
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 708a43c6..c7dcd007 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -121,7 +121,7 @@ $modal-view-button-icon-width: 3em;
 $modal-view-button-icon-margin: 0.5em;
 
 .modal-view.media-modal-view {
-  z-index: 1001;
+  z-index: 9000;
   flex-direction: column;
 
   .modal-view-button-arrow,
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index 2b58913f..9394efff 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -35,7 +35,7 @@ export default {
 
 <style lang="scss">
 .modal-view {
-  z-index: 1000;
+  z-index: 2000;
   position: fixed;
   top: 0;
   left: 0;
-- 
cgit v1.2.3-70-g09d2


From 3e1b40ce29feae8ab2b13da35eece15df8a7697c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 10 Apr 2022 20:18:54 +0300
Subject: fix new post button not working day1, minor stylistic fixes

---
 src/App.js                                                 | 14 ++++++++++++++
 src/App.scss                                               |  6 ++++++
 src/App.vue                                                |  2 +-
 src/components/chat/chat.js                                |  1 +
 src/components/chat/chat.scss                              |  3 +++
 .../mobile_post_status_button/mobile_post_status_button.js |  2 +-
 6 files changed, 26 insertions(+), 2 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index 75fa3a3c..39ef62e7 100644
--- a/src/App.js
+++ b/src/App.js
@@ -50,6 +50,16 @@ export default {
     window.removeEventListener('resize', this.updateMobileState)
   },
   computed: {
+    classes () {
+      return [
+        {
+          '-reverse': this.reverseLayout,
+          '-no-sticky-headers': this.noSticky,
+          '-has-new-post-button': this.newPostButtonShown
+        },
+        '-' + this.layoutType
+      ]
+    },
     currentUser () { return this.$store.state.users.currentUser },
     userBackground () { return this.currentUser.background_image },
     instanceBackground () {
@@ -72,6 +82,10 @@ export default {
         !this.$store.getters.mergedConfig.hideISP &&
         this.$store.state.instance.instanceSpecificPanelContent
     },
+    newPostButtonShown () {
+      if (this.$route.name === 'chat' || this.$route.name === 'chats') return false
+      return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
+    },
     showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
     shoutboxPosition () {
       return this.$store.getters.mergedConfig.showNewPostButton || false
diff --git a/src/App.scss b/src/App.scss
index e65f3b2a..c8470c8a 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -222,6 +222,12 @@ nav {
     }
   }
 
+  &.-has-new-post-button {
+    .column {
+      padding-bottom: 20em;
+    }
+  }
+
   &.-no-sticky-headers {
     .column {
       .panel-heading.-sticky {
diff --git a/src/App.vue b/src/App.vue
index b5f6e7d3..094a8ab9 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -13,7 +13,7 @@
     <div
       id="content"
       class="app-layout container"
-      :class="[{ '-reverse': reverseLayout, '-no-sticky-headers': noSticky }, '-' + layoutType]"
+      :class="classes"
     >
       <div class="underlay"/>
       <div id="sidebar" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }">
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index a2789d2e..9a0ae107 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -91,6 +91,7 @@ const Chat = {
     ...mapState({
       backendInteractor: state => state.api.backendInteractor,
       mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
+      mobileLayout: state => state.interface.layoutType === 'mobile',
       currentUser: state => state.users.currentUser
     })
   },
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 34a335fe..1bad51b9 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -1,6 +1,9 @@
 .chat-view {
   display: flex;
   height: 100%;
+  // Stick the form to the top/bottom of screen
+  margin-bottom: calc(var(--___columnMargin) / -2);
+  margin-top: calc(var(--___columnMargin) / -2);
 
   .chat-view-inner {
     height: auto;
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 4866ac57..ecf79b64 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
@@ -45,7 +45,7 @@ const MobilePostStatusButton = {
       return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
     },
     isPersistent () {
-      return !!this.$store.getters.mergedConfig.showNewPostButton
+      return !!this.$store.getters.mergedConfig.alwaysShowNewPostButton
     },
     autohideFloatingPostButton () {
       return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
-- 
cgit v1.2.3-70-g09d2


From 4f617a7293922fdce85462009feb7e52770a7e62 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 11 Apr 2022 23:30:41 +0300
Subject: fix for chat shadow

---
 src/App.js                    | 5 ++++-
 src/App.scss                  | 5 +++++
 src/App.vue                   | 2 +-
 src/components/chat/chat.scss | 4 +++-
 4 files changed, 13 insertions(+), 3 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index 39ef62e7..9cfaf4fa 100644
--- a/src/App.js
+++ b/src/App.js
@@ -82,8 +82,11 @@ export default {
         !this.$store.getters.mergedConfig.hideISP &&
         this.$store.state.instance.instanceSpecificPanelContent
     },
+    isChats () {
+      return this.$route.name === 'chat' || this.$route.name === 'chats'
+    },
     newPostButtonShown () {
-      if (this.$route.name === 'chat' || this.$route.name === 'chats') return false
+      if (this.isChats) return false
       return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
     },
     showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
diff --git a/src/App.scss b/src/App.scss
index b5411585..31d7fac2 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -207,6 +207,11 @@ nav {
       z-index: 2;
     }
 
+    &.-full-height {
+      margin-top: 0;
+      margin-bottom: 0;
+    }
+
     &.-scrollable {
       --___paddingIncrease: calc(var(--columnGap) / 2);
 
diff --git a/src/App.vue b/src/App.vue
index 094a8ab9..707b91e5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -26,7 +26,7 @@
           <div id="notifs-sidebar" />
         </template>
       </div>
-      <div id="main-scroller" class="column main">
+      <div id="main-scroller" class="column main" :class="{ '-full-height': isChats }">
         <div
           v-if="!currentUser"
           class="login-hint panel panel-default"
diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss
index 1bad51b9..7b4dd37f 100644
--- a/src/components/chat/chat.scss
+++ b/src/components/chat/chat.scss
@@ -22,6 +22,8 @@
     margin: 0 0 0 0;
     border-radius: 10px 10px 0 0;
     border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
+    margin-bottom: calc(var(--___columnMargin) * -1);
+    padding-bottom: var(--___columnMargin);
 
     &::after {
       border-radius: 0;
@@ -40,7 +42,7 @@
     bottom: 0;
     background-color: $fallback--bg;
     background-color: var(--bg, $fallback--bg);
-    z-index: 10;
+    z-index: 1;
   }
 
   .chat-view-heading {
-- 
cgit v1.2.3-70-g09d2


From 49db16318b8072cf15f2ffd5a8a237ee86034b74 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 26 Apr 2022 18:12:45 +0300
Subject: more fixes for shoutbox

---
 src/App.js                                 |  2 +-
 src/App.vue                                |  2 +-
 src/components/shout_panel/shout_panel.vue | 19 ++++++++++---------
 3 files changed, 12 insertions(+), 11 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index 19c58595..f01f8788 100644
--- a/src/App.js
+++ b/src/App.js
@@ -91,7 +91,7 @@ export default {
     },
     showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
     shoutboxPosition () {
-      return this.$store.getters.mergedConfig.showNewPostButton || false
+      return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
     },
     hideShoutbox () {
       return this.$store.getters.mergedConfig.hideShoutbox
diff --git a/src/App.vue b/src/App.vue
index 707b91e5..5b448972 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -47,7 +47,7 @@
       v-if="currentUser && shout && !hideShoutbox"
       :floating="true"
       class="floating-shout mobile-hidden"
-      :class="{ 'left': shoutboxPosition }"
+      :class="{ '-left': shoutboxPosition }"
     />
     <MobilePostStatusButton />
     <UserReportingModal />
diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue
index f3e9ff96..1eca88a7 100644
--- a/src/components/shout_panel/shout_panel.vue
+++ b/src/components/shout_panel/shout_panel.vue
@@ -57,7 +57,7 @@
   >
     <div class="panel panel-default">
       <div
-        class="panel-heading stub timeline-heading shout-heading"
+        class="panel-heading -stub timeline-heading shout-heading"
         @click.stop.prevent="togglePanel"
       >
         <div class="title">
@@ -79,17 +79,17 @@
 
 .floating-shout {
   position: fixed;
-  bottom: 0px;
+  bottom: 0.5em;
   z-index: 1000;
   max-width: 25em;
-}
 
-.floating-shout.left {
-  left: 0px;
-}
+  &.-left {
+    left: 0.5em;
+  }
 
-.floating-shout:not(.left) {
-  right: 0px;
+  &:not(.-left) {
+    right: 0.5em;
+  }
 }
 
 .shout-panel {
@@ -121,7 +121,7 @@
 
   .shout-message {
     display: flex;
-    padding: 0.2em 0.5em
+    padding: 0.2em 0.5em;
   }
 
   .shout-avatar {
@@ -137,6 +137,7 @@
 
   .shout-input {
     display: flex;
+
     textarea {
       flex: 1;
       margin: 0.6em;
-- 
cgit v1.2.3-70-g09d2


From fd831a27f4e9c6bcd5c40f3449d63546fba2560d Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 20 May 2022 00:56:23 +0300
Subject: experimental disjointed popups

---
 src/App.vue                                  |  1 +
 src/components/popover/popover.js            | 10 +++++++++-
 src/components/popover/popover.vue           | 28 +++++++++++++++-------------
 src/components/react_button/react_button.vue |  1 +
 4 files changed, 26 insertions(+), 14 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 5b448972..c217ade5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -54,6 +54,7 @@
     <PostStatusModal />
     <SettingsModal />
     <div id="modal" />
+    <div id="popovers" />
     <GlobalNoticeList />
   </div>
 </template>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index a30a37c9..db283ef0 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -43,6 +43,7 @@ const Popover = {
   methods: {
     containerBoundingClientRect () {
       const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
+      console.log(container)
       return container.getBoundingClientRect()
     },
     updateStyles () {
@@ -125,10 +126,17 @@ const Popover = {
 
       // Note, separate translateX and translateY avoids blurry text on chromium,
       // single translate or translate3d resulted in blurry text.
+      console.log(translateX + screenBox.x + screenBox.width)
+      console.log(Math.round(parentBounds.width))
       this.styles = {
         opacity: 1,
-        transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
+        // transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
+        left: `${Math.round(translateX + screenBox.x + screenBox.width / 2)}px`,
+        top: `${Math.round(translateY + screenBox.y + screenBox.height / 2)}px`,
+        maxWidth: `${Math.round(parentBounds.width)}px`,
+        position: 'fixed'
       }
+      console.log(this.styles)
     },
     showPopover () {
       const wasHidden = this.hidden
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index c2a3e801..8f6ef2e6 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -11,19 +11,21 @@
     >
       <slot name="trigger" />
     </button>
-    <div
-      v-if="!hidden"
-      ref="content"
-      :style="styles"
-      class="popover"
-      :class="popoverClass || 'popover-default'"
-    >
-      <slot
-        name="content"
-        class="popover-inner"
-        :close="hidePopover"
-      />
-    </div>
+    <teleport to="#popovers">
+      <div
+        v-if="!hidden"
+        ref="content"
+        :style="styles"
+        class="popover"
+        :class="popoverClass || 'popover-default'"
+      >
+        <slot
+          name="content"
+          class="popover-inner"
+          :close="hidePopover"
+        />
+      </div>
+    </teleport>
   </div>
 </template>
 
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index 7f35b7b5..8da691c0 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -6,6 +6,7 @@
     :offset="{ y: 5 }"
     :bound-to="{ x: 'container' }"
     remove-padding
+    popover-class="ReactButton popover-default"
     @show="focusInput"
   >
     <template v-slot:content="{close}">
-- 
cgit v1.2.3-70-g09d2


From 9b5fe24ca455af5420a326e78878ca7b35b5bf66 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 7 Jun 2022 16:52:03 +0300
Subject: restore notifications page, fix z-index issues

---
 src/App.vue                                    | 2 +-
 src/boot/routes.js                             | 2 +-
 src/components/notifications/notifications.js  | 4 +++-
 src/components/notifications/notifications.vue | 2 +-
 src/components/timeline/timeline.scss          | 2 +-
 5 files changed, 7 insertions(+), 5 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 5b448972..21f6f686 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -9,7 +9,7 @@
     />
     <MobileNav v-if="layoutType === 'mobile'" />
     <DesktopNav v-else />
-    <notifications v-if="currentUser" />
+    <Notifications v-if="currentUser" />
     <div
       id="content"
       class="app-layout container"
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 905ffe41..726476a8 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -62,7 +62,7 @@ export default (store) => {
     { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
     { name: 'registration-token', path: '/registration/:token', component: Registration },
     { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
-    { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
+    { name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
     { name: 'login', path: '/login', component: AuthForm },
     { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
     { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index fb2579a5..82aa1489 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -27,7 +27,9 @@ const Notifications = {
     // meant for "Interactions" timeline
     minimalMode: Boolean,
     // Custom filter mode, an array of strings, possible values 'mention', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline
-    filterMode: Array
+    filterMode: Array,
+    // Disable teleporting (i.e. for /users/user/notifications)
+    disableTeleport: Boolean
   },
   data () {
     return {
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 794ef51d..b46c06aa 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -1,5 +1,5 @@
 <template>
-  <teleport :disabled="minimalMode" :to="teleportTarget">
+  <teleport :disabled="minimalMode || disableTeleport" :to="teleportTarget">
     <div
       :class="{ minimal: minimalMode }"
       class="Notifications"
diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss
index c7ea0dff..9e009fd3 100644
--- a/src/components/timeline/timeline.scss
+++ b/src/components/timeline/timeline.scss
@@ -11,7 +11,7 @@
 
   .conversation-heading {
     top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 2));
-    z-index: 1;
+    z-index: 2;
   }
 
   &.-nonpanel {
-- 
cgit v1.2.3-70-g09d2


From 1a333aabba759fb68449ead9c47e986f456e8c8c Mon Sep 17 00:00:00 2001
From: Sean King <seanking2919@protonmail.com>
Date: Tue, 7 Jun 2022 21:31:48 -0600
Subject: Add edit status functionality

---
 CONTRIBUTORS.md                                    |  1 +
 src/App.js                                         |  2 +
 src/App.vue                                        |  1 +
 .../edit_status_modal/edit_status_modal.js         | 75 ++++++++++++++++++
 .../edit_status_modal/edit_status_modal.vue        | 48 ++++++++++++
 src/components/extra_buttons/extra_buttons.js      | 13 ++++
 src/components/extra_buttons/extra_buttons.vue     | 11 +++
 .../post_status_form/post_status_form.js           | 45 ++++++++---
 .../post_status_form/post_status_form.vue          |  1 +
 src/i18n/en.json                                   |  2 +
 src/main.js                                        |  3 +
 src/modules/api.js                                 |  2 +
 src/modules/editStatus.js                          | 25 ++++++
 src/modules/statuses.js                            |  6 ++
 src/services/api/api.service.js                    | 88 +++++++++++++++++++++-
 .../entity_normalizer/entity_normalizer.service.js | 12 +++
 .../status_poster/status_poster.service.js         | 42 +++++++++++
 17 files changed, 365 insertions(+), 12 deletions(-)
 create mode 100644 src/components/edit_status_modal/edit_status_modal.js
 create mode 100644 src/components/edit_status_modal/edit_status_modal.vue
 create mode 100644 src/modules/editStatus.js

(limited to 'src/App.vue')

diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f666a4ef..18f4a930 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -10,3 +10,4 @@ Contributors of this project.
 - shpuld (shpuld@shitposter.club): CSS and styling
 - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
 - hj (hj@shigusegubu.club): Code
+- Sean King (seanking@freespeechextremist.com): Code
diff --git a/src/App.js b/src/App.js
index f01f8788..6e0e34a8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -11,6 +11,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
 import MobileNav from './components/mobile_nav/mobile_nav.vue'
 import DesktopNav from './components/desktop_nav/desktop_nav.vue'
 import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
+import EditStatusModal from './components/edit_status_modal/edit_status_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, windowHeight } from './services/window_utils/window_utils'
@@ -35,6 +36,7 @@ export default {
     SettingsModal,
     UserReportingModal,
     PostStatusModal,
+    EditStatusModal,
     GlobalNoticeList
   },
   data: () => ({
diff --git a/src/App.vue b/src/App.vue
index 5b448972..9484f993 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -52,6 +52,7 @@
     <MobilePostStatusButton />
     <UserReportingModal />
     <PostStatusModal />
+    <EditStatusModal />
     <SettingsModal />
     <div id="modal" />
     <GlobalNoticeList />
diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js
new file mode 100644
index 00000000..14320d21
--- /dev/null
+++ b/src/components/edit_status_modal/edit_status_modal.js
@@ -0,0 +1,75 @@
+import PostStatusForm from '../post_status_form/post_status_form.vue'
+import Modal from '../modal/modal.vue'
+import statusPosterService from '../../services/status_poster/status_poster.service.js'
+import get from 'lodash/get'
+
+const EditStatusModal = {
+  components: {
+    PostStatusForm,
+    Modal
+  },
+  data () {
+    return {
+      resettingForm: false
+    }
+  },
+  computed: {
+    isLoggedIn () {
+      return !!this.$store.state.users.currentUser
+    },
+    modalActivated () {
+      return this.$store.state.editStatus.modalActivated
+    },
+    isFormVisible () {
+      return this.isLoggedIn && !this.resettingForm && this.modalActivated
+    },
+    params () {
+      return this.$store.state.editStatus.params || {}
+    }
+  },
+  watch: {
+    params (newVal, oldVal) {
+      if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) {
+        this.resettingForm = true
+        this.$nextTick(() => {
+          this.resettingForm = false
+        })
+      }
+    },
+    isFormVisible (val) {
+      if (val) {
+        this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
+      }
+    }
+  },
+  methods: {
+    doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
+      const params = {
+        store: this.$store,
+        statusId: this.$store.state.editStatus.params.statusId,
+        status,
+        spoilerText,
+        sensitive,
+        poll,
+        media,
+        contentType
+      }
+
+      return statusPosterService.editStatus(params)
+        .then((data) => {
+          return data
+        })
+        .catch((err) => {
+          console.error('Error editing status', err)
+          return {
+            error: err.message
+          }
+        })
+    },
+    closeModal () {
+      this.$store.dispatch('closeEditStatusModal')
+    }
+  }
+}
+
+export default EditStatusModal
diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue
new file mode 100644
index 00000000..00dde7de
--- /dev/null
+++ b/src/components/edit_status_modal/edit_status_modal.vue
@@ -0,0 +1,48 @@
+<template>
+  <Modal
+    v-if="isFormVisible"
+    class="edit-form-modal-view"
+    @backdropClicked="closeModal"
+  >
+    <div class="edit-form-modal-panel panel">
+      <div class="panel-heading">
+        {{ $t('post_status.edit_status') }}
+      </div>
+      <PostStatusForm
+        class="panel-body"
+        v-bind="params"
+        @posted="closeModal"
+        :disablePolls="true"
+        :disableVisibilitySelector="true"
+        :post-handler="doEditStatus"
+      />
+    </div>
+  </Modal>
+</template>
+
+<script src="./edit_status_modal.js"></script>
+
+<style lang="scss">
+.modal-view.edit-form-modal-view {
+  align-items: flex-start;
+}
+.edit-form-modal-panel {
+  flex-shrink: 0;
+  margin-top: 25%;
+  margin-bottom: 2em;
+  width: 100%;
+  max-width: 700px;
+
+  @media (orientation: landscape) {
+    margin-top: 8%;
+  }
+
+  .form-bottom-left {
+    max-width: 6.5em;
+
+    .emoji-icon {
+      justify-content: right;
+    }
+  }
+}
+</style>
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index dd45b6b9..9508a707 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -71,6 +71,19 @@ const ExtraButtons = {
     },
     reportStatus () {
       this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
+    },
+    editStatus () {
+      this.$store.dispatch('fetchStatusSource', { id: this.status.id })
+        .then(data => this.$store.dispatch('openEditStatusModal', {
+          statusId: this.status.id,
+          subject: data.spoiler_text,
+          statusText: data.text,
+          statusIsSensitive: this.status.nsfw,
+          statusPoll: this.status.poll,
+          statusFiles: this.status.attachments,
+          visibility: this.status.visibility,
+          statusContentType: data.content_type
+        }))
     }
   },
   computed: {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index a3c3c767..8e90ee27 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -73,6 +73,17 @@
             icon="bookmark"
           /><span>{{ $t("status.unbookmark") }}</span>
         </button>
+        <button
+          v-if="ownStatus"
+          class="button-default dropdown-item dropdown-item-icon"
+          @click.prevent="editStatus"
+          @click="close"
+        >
+          <FAIcon
+            fixed-width
+            icon="pen"
+          /><span>{{ $t("status.edit") }}</span>
+        </button>
         <button
           v-if="canDelete"
           class="button-default dropdown-item dropdown-item-icon"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 2febf226..d73219ad 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
 
 const PostStatusForm = {
   props: [
+    'statusId',
+    'statusText',
+    'statusIsSensitive',
+    'statusPoll',
+    'statusFiles',
+    'statusMediaDescriptions',
+    'statusScope',
+    'statusContentType',
     'replyTo',
     'repliedUser',
     'attentions',
@@ -62,6 +70,7 @@ const PostStatusForm = {
     'subject',
     'disableSubject',
     'disableScopeSelector',
+    'disableVisibilitySelector',
     'disableNotice',
     'disableLockWarning',
     'disablePolls',
@@ -125,22 +134,38 @@ const PostStatusForm = {
 
     const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
 
+    let statusParams = {
+      spoilerText: this.subject || '',
+      status: statusText,
+      nsfw: !!sensitiveByDefault,
+      files: [],
+      poll: {},
+      mediaDescriptions: {},
+      visibility: scope,
+      contentType
+    }
+
+    if (this.statusId) {
+      const statusContentType = this.statusContentType || contentType
+      statusParams = {
+        spoilerText: this.subject || '',
+        status: this.statusText || '',
+        nsfw: this.statusIsSensitive || !!sensitiveByDefault,
+        files: this.statusFiles || [],
+        poll: this.statusPoll || {},
+        mediaDescriptions: this.statusMediaDescriptions || {},
+        visibility: this.statusScope || scope,
+        contentType: statusContentType
+      }
+    }
+
     return {
       dropFiles: [],
       uploadingFiles: false,
       error: null,
       posting: false,
       highlighted: 0,
-      newStatus: {
-        spoilerText: this.subject || '',
-        status: statusText,
-        nsfw: !!sensitiveByDefault,
-        files: [],
-        poll: {},
-        mediaDescriptions: {},
-        visibility: scope,
-        contentType
-      },
+      newStatus: statusParams,
       caret: 0,
       pollFormVisible: false,
       showDropIcon: 'hide',
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 62613bd1..60cab745 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -170,6 +170,7 @@
           class="visibility-tray"
         >
           <scope-selector
+            v-if="!disableVisibilitySelector"
             :show-all="showAllScopes"
             :user-default="userDefaultScope"
             :original-scope="copyMessageScope"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0cfda804..fa773982 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -200,6 +200,7 @@
     "load_older": "Load older interactions"
   },
   "post_status": {
+    "edit_status": "Edit status",
     "new_status": "Post new status",
     "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
     "account_not_locked_warning_link": "locked",
@@ -744,6 +745,7 @@
     "favorites": "Favorites",
     "repeats": "Repeats",
     "delete": "Delete status",
+    "edit": "Edit status",
     "pin": "Pin on profile",
     "unpin": "Unpin from profile",
     "pinned": "Pinned",
diff --git a/src/main.js b/src/main.js
index eacd554c..f11b80ed 100644
--- a/src/main.js
+++ b/src/main.js
@@ -18,6 +18,8 @@ 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 editStatusModule from './modules/editStatus.js'
+
 import chatsModule from './modules/chats.js'
 
 import { createI18n } from 'vue-i18n'
@@ -81,6 +83,7 @@ const persistedStateOptions = {
       reports: reportsModule,
       polls: pollsModule,
       postStatus: postStatusModule,
+      editStatus: editStatusModule,
       chats: chatsModule
     },
     plugins,
diff --git a/src/modules/api.js b/src/modules/api.js
index 54f94356..e23cc030 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -100,6 +100,8 @@ const api = {
                   showImmediately: timelineData.visibleStatuses.length === 0,
                   timeline: 'friends'
                 })
+              } else if (message.event === 'status.update') {
+                // Insert dispatch code here.
               } else if (message.event === 'delete') {
                 dispatch('deleteStatusById', message.id)
               } else if (message.event === 'pleroma:chat_update') {
diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js
new file mode 100644
index 00000000..fd316519
--- /dev/null
+++ b/src/modules/editStatus.js
@@ -0,0 +1,25 @@
+const editStatus = {
+  state: {
+    params: null,
+    modalActivated: false
+  },
+  mutations: {
+    openEditStatusModal (state, params) {
+      state.params = params
+      state.modalActivated = true
+    },
+    closeEditStatusModal (state) {
+      state.modalActivated = false
+    }
+  },
+  actions: {
+    openEditStatusModal ({ commit }, params) {
+      commit('openEditStatusModal', params)
+    },
+    closeEditStatusModal ({ commit }) {
+      commit('closeEditStatusModal')
+    }
+  }
+}
+
+export default editStatus
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index a13930e9..19220442 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -248,6 +248,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
     'status': (status) => {
       addStatus(status, showImmediately)
     },
+    'edit': (status) => {
+      addStatus(status, showImmediately)
+    },
     'retweet': (status) => {
       // RetweetedStatuses are never shown immediately
       const retweetedStatus = addStatus(status.retweeted_status, false, false)
@@ -600,6 +603,9 @@ const statuses = {
       return rootState.api.backendInteractor.fetchStatus({ id })
         .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
     },
+    fetchStatusSource ({ rootState, dispatch }, status) {
+      return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
+    },
     deleteStatus ({ rootState, commit }, status) {
       commit('setDeleted', { status })
       apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ca84ba2c..f0ef4576 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, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
@@ -47,6 +47,8 @@ const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
 const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
 const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
 const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
+const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
+const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history`
 const MASTODON_USER_URL = '/api/v1/accounts'
 const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
 const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
@@ -410,6 +412,32 @@ const fetchStatus = ({ id, credentials }) => {
     .then((data) => parseStatus(data))
 }
 
+const fetchStatusSource = ({ id, credentials }) => {
+  let url = MASTODON_STATUS_SOURCE_URL(id)
+  return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching source', data)
+    })
+    .then((data) => data.json())
+    .then((data) => parseSource(data))
+}
+
+const fetchStatusHistory = ({ id, credentials }) => {
+  let url = MASTODON_STATUS_HISTORY_URL(id)
+  return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching history', data)
+    })
+    .then((data) => data.json())
+    .then((data) => parseStatus(data))
+}
+
 const tagUser = ({ tag, credentials, user }) => {
   const screenName = user.screen_name
   const form = {
@@ -701,6 +729,54 @@ const postStatus = ({
     .then((data) => data.error ? data : parseStatus(data))
 }
 
+const editStatus = ({
+  id,
+  credentials,
+  status,
+  spoilerText,
+  sensitive,
+  poll,
+  mediaIds = [],
+  contentType
+}) => {
+  const form = new FormData()
+  const pollOptions = poll.options || []
+
+  form.append('status', status)
+  if (spoilerText) form.append('spoiler_text', spoilerText)
+  if (sensitive) form.append('sensitive', sensitive)
+  if (contentType) form.append('content_type', contentType)
+  mediaIds.forEach(val => {
+    form.append('media_ids[]', val)
+  })
+
+  if (pollOptions.some(option => option !== '')) {
+    const normalizedPoll = {
+      expires_in: poll.expiresIn,
+      multiple: poll.multiple
+    }
+    Object.keys(normalizedPoll).forEach(key => {
+      form.append(`poll[${key}]`, normalizedPoll[key])
+    })
+
+    pollOptions.forEach(option => {
+      form.append('poll[options][]', option)
+    })
+  }
+
+  let putHeaders = authHeaders(credentials)
+
+  return fetch(MASTODON_STATUS_URL(id), {
+    body: form,
+    method: 'PUT',
+    headers: putHeaders
+  })
+    .then((response) => {
+      return response.json()
+    })
+    .then((data) => data.error ? data : parseStatus(data))
+}
+
 const deleteStatus = ({ id, credentials }) => {
   return fetch(MASTODON_DELETE_URL(id), {
     headers: authHeaders(credentials),
@@ -1105,9 +1181,12 @@ const MASTODON_STREAMING_EVENTS = new Set([
   'update',
   'notification',
   'delete',
-  'filters_changed'
+  'filters_changed',
+  'status.update'
 ])
 
+// If Mastodon is doing a different streaming event,
+// please let us know, Gargron.
 const PLEROMA_STREAMING_EVENTS = new Set([
   'pleroma:chat_update'
 ])
@@ -1177,6 +1256,8 @@ export const handleMastoWS = (wsEvent) => {
     const data = payload ? JSON.parse(payload) : null
     if (event === 'update') {
       return { event, status: parseStatus(data) }
+    } else if (event === 'status.update') {
+      return { event, status: parseStatus(data) }
     } else if (event === 'notification') {
       return { event, notification: parseNotification(data) }
     } else if (event === 'pleroma:chat_update') {
@@ -1279,6 +1360,8 @@ const apiService = {
   fetchPinnedStatuses,
   fetchConversation,
   fetchStatus,
+  fetchStatusSource,
+  fetchStatusHistory,
   fetchFriends,
   exportFriends,
   fetchFollowers,
@@ -1299,6 +1382,7 @@ const apiService = {
   bookmarkStatus,
   unbookmarkStatus,
   postStatus,
+  editStatus,
   deleteStatus,
   uploadMedia,
   setMediaDescription,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index f219c161..82c19655 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -244,6 +244,16 @@ export const parseAttachment = (data) => {
   return output
 }
 
+export const parseSource = (data) => {
+  const output = {}
+
+  output.text = data.text
+  output.spoiler_text = data.spoiler_text
+  output.content_type = data.content_type
+
+  return output
+}
+
 export const parseStatus = (data) => {
   const output = {}
   const masto = data.hasOwnProperty('account')
@@ -265,6 +275,8 @@ export const parseStatus = (data) => {
 
     output.tags = data.tags
 
+    output.is_edited = data.edited_at !== null
+
     if (data.pleroma) {
       const { pleroma } = data
       output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index f09196aa..1eb10bb6 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -47,6 +47,47 @@ const postStatus = ({
     })
 }
 
+const editStatus = ({
+  store,
+  statusId,
+  status,
+  spoilerText,
+  sensitive,
+  poll,
+  media = [],
+  contentType = 'text/plain'
+}) => {
+  const mediaIds = map(media, 'id')
+
+  return apiService.editStatus({
+    id: statusId,
+    credentials: store.state.users.currentUser.credentials,
+    status,
+    spoilerText,
+    sensitive,
+    poll,
+    mediaIds,
+    contentType
+  })
+    .then((data) => {
+      if (!data.error) {
+        store.dispatch('addNewStatuses', {
+          statuses: [data],
+          timeline: 'friends',
+          showImmediately: true,
+          noIdUpdate: true // To prevent missing notices on next pull.
+        })
+      }
+      return data
+    })
+    .catch((err) => {
+      console.error('Error editing status', err)
+      return {
+        error: err.message
+      }
+    })
+}
+
 const uploadMedia = ({ store, formData }) => {
   const credentials = store.state.users.currentUser.credentials
   return apiService.uploadMedia({ credentials, formData })
@@ -59,6 +100,7 @@ const setMediaDescription = ({ store, id, description }) => {
 
 const statusPosterService = {
   postStatus,
+  editStatus,
   uploadMedia,
   setMediaDescription
 }
-- 
cgit v1.2.3-70-g09d2


From 97e4a3d8e52bc2255f4be23967a2223f5e458a67 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jun 2022 01:36:41 +0300
Subject: fix popovers in modals

---
 src/App.js                         | 3 +--
 src/App.vue                        | 2 +-
 src/components/popover/popover.vue | 2 +-
 3 files changed, 3 insertions(+), 4 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index f01f8788..d5967685 100644
--- a/src/App.js
+++ b/src/App.js
@@ -4,7 +4,6 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
 import FeaturesPanel from './components/features_panel/features_panel.vue'
 import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
 import ShoutPanel from './components/shout_panel/shout_panel.vue'
-import SettingsModal from './components/settings_modal/settings_modal.vue'
 import MediaModal from './components/media_modal/media_modal.vue'
 import SideDrawer from './components/side_drawer/side_drawer.vue'
 import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@@ -32,7 +31,7 @@ export default {
     MobilePostStatusButton,
     MobileNav,
     DesktopNav,
-    SettingsModal,
+    SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
     UserReportingModal,
     PostStatusModal,
     GlobalNoticeList
diff --git a/src/App.vue b/src/App.vue
index c217ade5..5d7bb4e1 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -54,8 +54,8 @@
     <PostStatusModal />
     <SettingsModal />
     <div id="modal" />
-    <div id="popovers" />
     <GlobalNoticeList />
+    <div id="popovers" />
   </div>
 </template>
 
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 528c4fb2..25ce32fb 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -43,7 +43,7 @@
 }
 
 .popover {
-  z-index: 500;
+  z-index: 90000;
   position: absolute;
   min-width: 0;
   box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
-- 
cgit v1.2.3-70-g09d2


From aca8cf64db26e8a883a8099462ce5381c7e330e4 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 21 Jun 2022 00:13:13 +0300
Subject: move media modal to the topmost layer

---
 src/App.vue                                | 2 +-
 src/components/media_modal/media_modal.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 5d7bb4e1..741786ec 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -42,7 +42,7 @@
       </div>
       <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
     </div>
-    <media-modal />
+    <MediaModal />
     <shout-panel
       v-if="currentUser && shout && !hideShoutbox"
       :floating="true"
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 8b76aafb..b93d3d84 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -121,7 +121,7 @@ $modal-view-button-icon-width: 3em;
 $modal-view-button-icon-margin: 0.5em;
 
 .modal-view.media-modal-view {
-  z-index: 9000;
+  z-index: 900000;
   flex-direction: column;
 
   .modal-view-button-arrow,
-- 
cgit v1.2.3-70-g09d2


From fa5d35523dd081b6948d38325374cac5707b7868 Mon Sep 17 00:00:00 2001
From: Sean King <seanking2919@protonmail.com>
Date: Mon, 20 Jun 2022 22:52:08 -0600
Subject: Add ability to view status history for edited statuses

---
 src/App.js                                         |  2 +
 src/App.vue                                        |  1 +
 src/components/extra_buttons/extra_buttons.js      | 23 ++++++++-
 src/components/extra_buttons/extra_buttons.vue     | 11 ++++
 .../status_history_modal/status_history_modal.js   | 60 ++++++++++++++++++++++
 .../status_history_modal/status_history_modal.vue  | 46 +++++++++++++++++
 src/i18n/en.json                                   |  3 +-
 src/main.js                                        |  2 +
 src/modules/statusHistory.js                       | 25 +++++++++
 src/modules/statuses.js                            |  3 ++
 src/services/api/api.service.js                    | 17 +++---
 .../entity_normalizer/entity_normalizer.service.js |  6 ++-
 12 files changed, 186 insertions(+), 13 deletions(-)
 create mode 100644 src/components/status_history_modal/status_history_modal.js
 create mode 100644 src/components/status_history_modal/status_history_modal.vue
 create mode 100644 src/modules/statusHistory.js

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index 6e0e34a8..af638a1c 100644
--- a/src/App.js
+++ b/src/App.js
@@ -13,6 +13,7 @@ import DesktopNav from './components/desktop_nav/desktop_nav.vue'
 import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
 import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
 import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
+import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
 import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
 import { windowWidth, windowHeight } from './services/window_utils/window_utils'
 import { mapGetters } from 'vuex'
@@ -37,6 +38,7 @@ export default {
     UserReportingModal,
     PostStatusModal,
     EditStatusModal,
+    StatusHistoryModal,
     GlobalNoticeList
   },
   data: () => ({
diff --git a/src/App.vue b/src/App.vue
index 9484f993..1b513e08 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -53,6 +53,7 @@
     <UserReportingModal />
     <PostStatusModal />
     <EditStatusModal />
+    <StatusHistoryModal />
     <SettingsModal />
     <div id="modal" />
     <GlobalNoticeList />
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 8a703953..11fe3235 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -6,7 +6,8 @@ import {
   faEyeSlash,
   faThumbtack,
   faShareAlt,
-  faExternalLinkAlt
+  faExternalLinkAlt,
+  faHistory
 } from '@fortawesome/free-solid-svg-icons'
 import {
   faBookmark as faBookmarkReg,
@@ -21,7 +22,8 @@ library.add(
   faThumbtack,
   faShareAlt,
   faExternalLinkAlt,
-  faFlag
+  faFlag,
+  faHistory
 )
 
 const ExtraButtons = {
@@ -84,6 +86,20 @@ const ExtraButtons = {
           visibility: this.status.visibility,
           statusContentType: data.content_type
         }))
+    },
+    showStatusHistory () {
+      let originalStatus = {}
+      Object.assign(originalStatus, this.status)
+      delete originalStatus.attachments
+      delete originalStatus.created_at
+      delete originalStatus.emojis
+      delete originalStatus.text
+      delete originalStatus.raw_html
+      delete originalStatus.nsfw
+      delete originalStatus.poll
+      delete originalStatus.summary
+      delete originalStatus.summary_raw_html
+      this.$store.dispatch('openStatusHistoryModal', originalStatus)
     }
   },
   computed: {
@@ -104,6 +120,9 @@ const ExtraButtons = {
     },
     statusLink () {
       return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
+    },
+    isEdited () {
+      return this.status.edited_at !== null
     }
   }
 }
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 8e90ee27..877e1424 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -84,6 +84,17 @@
             icon="pen"
           /><span>{{ $t("status.edit") }}</span>
         </button>
+        <button
+          v-if="isEdited"
+          class="button-default dropdown-item dropdown-item-icon"
+          @click.prevent="showStatusHistory"
+          @click="close"
+        >
+          <FAIcon
+            fixed-width
+            icon="history"
+          /><span>{{ $t("status.status_history") }}</span>
+        </button>
         <button
           v-if="canDelete"
           class="button-default dropdown-item dropdown-item-icon"
diff --git a/src/components/status_history_modal/status_history_modal.js b/src/components/status_history_modal/status_history_modal.js
new file mode 100644
index 00000000..3941a56f
--- /dev/null
+++ b/src/components/status_history_modal/status_history_modal.js
@@ -0,0 +1,60 @@
+import { get } from 'lodash'
+import Modal from '../modal/modal.vue'
+import Status from '../status/status.vue'
+
+const StatusHistoryModal = {
+  components: {
+    Modal,
+    Status
+  },
+  data () {
+    return {
+      statuses: []
+    }
+  },
+  computed: {
+    modalActivated () {
+      return this.$store.state.statusHistory.modalActivated
+    },
+    params () {
+      return this.$store.state.statusHistory.params
+    },
+    statusId () {
+      return this.params.id
+    },
+    historyCount () {
+      return this.statuses.length
+    },
+    history () {
+      return this.statuses
+    }
+  },
+  watch: {
+    params (newVal, oldVal) {
+      const newStatusId = get(newVal, 'id') !== get(oldVal, 'id')
+      if (newStatusId) {
+        this.resetHistory()
+      }
+
+      if (newStatusId || get(newVal, 'edited_at') !== get(oldVal, 'edited_at')) {
+        this.fetchStatusHistory()
+      }
+    }
+  },
+  methods: {
+    resetHistory () {
+      this.statuses = []
+    },
+    fetchStatusHistory () {
+      this.$store.dispatch('fetchStatusHistory', this.params)
+        .then(data => {
+          this.statuses = data
+        })
+    },
+    closeModal () {
+      this.$store.dispatch('closeStatusHistoryModal')
+    }
+  }
+}
+
+export default StatusHistoryModal
diff --git a/src/components/status_history_modal/status_history_modal.vue b/src/components/status_history_modal/status_history_modal.vue
new file mode 100644
index 00000000..d6680df2
--- /dev/null
+++ b/src/components/status_history_modal/status_history_modal.vue
@@ -0,0 +1,46 @@
+<template>
+  <Modal
+    v-if="modalActivated"
+    class="status-history-modal-view"
+    @backdropClicked="closeModal"
+  >
+    <div class="status-history-modal-panel panel">
+      <div class="panel-heading">
+        {{ $t('status.status_history') }} ({{ historyCount }})
+      </div>
+      <div class="panel-body">
+        <div
+          v-if="historyCount > 0"
+          class="history-body"
+        >
+          <status
+            v-for="status in history"
+            :key="status.id"
+            :statusoid="status"
+            :isPreview="true"
+            class="conversation-status status-fadein panel-body"
+        />
+        </div>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<script src="./status_history_modal.js"></script>
+
+<style lang="scss">
+.modal-view.status-history-modal-view {
+  align-items: flex-start;
+}
+.status-history-modal-panel {
+  flex-shrink: 0;
+  margin-top: 25%;
+  margin-bottom: 2em;
+  width: 100%;
+  max-width: 700px;
+
+  @media (orientation: landscape) {
+    margin-top: 8%;
+  }
+}
+</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7b1bac79..f125c249 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -795,7 +795,8 @@
     "ancestor_follow_with_icon": "{icon} {text}",
     "show_all_conversation_with_icon": "{icon} {text}",
     "show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
-    "show_only_conversation_under_this": "Only show replies to this status"
+    "show_only_conversation_under_this": "Only show replies to this status",
+    "status_history": "Status history"
   },
   "user_card": {
     "approve": "Approve",
diff --git a/src/main.js b/src/main.js
index f11b80ed..032b4c58 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,6 +19,7 @@ import reportsModule from './modules/reports.js'
 import pollsModule from './modules/polls.js'
 import postStatusModule from './modules/postStatus.js'
 import editStatusModule from './modules/editStatus.js'
+import statusHistoryModule from './modules/statusHistory.js'
 
 import chatsModule from './modules/chats.js'
 
@@ -84,6 +85,7 @@ const persistedStateOptions = {
       polls: pollsModule,
       postStatus: postStatusModule,
       editStatus: editStatusModule,
+      statusHistory: statusHistoryModule,
       chats: chatsModule
     },
     plugins,
diff --git a/src/modules/statusHistory.js b/src/modules/statusHistory.js
new file mode 100644
index 00000000..db3d6d4b
--- /dev/null
+++ b/src/modules/statusHistory.js
@@ -0,0 +1,25 @@
+const statusHistory = {
+  state: {
+    params: {},
+    modalActivated: false
+  },
+  mutations: {
+    openStatusHistoryModal (state, params) {
+      state.params = params
+      state.modalActivated = true
+    },
+    closeStatusHistoryModal (state) {
+      state.modalActivated = false
+    }
+  },
+  actions: {
+    openStatusHistoryModal ({ commit }, params) {
+      commit('openStatusHistoryModal', params)
+    },
+    closeStatusHistoryModal ({ commit }) {
+      commit('closeStatusHistoryModal')
+    }
+  }
+}
+
+export default statusHistory
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 19220442..a2e27b87 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -606,6 +606,9 @@ const statuses = {
     fetchStatusSource ({ rootState, dispatch }, status) {
       return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
     },
+    fetchStatusHistory ({ rootState, dispatch }, status) {
+      return apiService.fetchStatusHistory({ status })
+    },
     deleteStatus ({ rootState, commit }, status) {
       commit('setDeleted', { status })
       apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 49b7e95a..9bc420c3 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -425,17 +425,16 @@ const fetchStatusSource = ({ id, credentials }) => {
     .then((data) => parseSource(data))
 }
 
-const fetchStatusHistory = ({ id, credentials }) => {
-  let url = MASTODON_STATUS_HISTORY_URL(id)
-  return fetch(url, { headers: authHeaders(credentials) })
+const fetchStatusHistory = ({ status, credentials }) => {
+  let url = MASTODON_STATUS_HISTORY_URL(status.id)
+  return promisedRequest({ url, credentials })
     .then((data) => {
-      if (data.ok) {
-        return data
-      }
-      throw new Error('Error fetching history', data)
+      data.reverse()
+      return data.map((item) => {
+        item.originalStatus = status
+        return parseStatus(item)
+      })
     })
-    .then((data) => data.json())
-    .then((data) => parseStatus(data))
 }
 
 const tagUser = ({ tag, credentials, user }) => {
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 82c19655..2a186ba1 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -275,7 +275,7 @@ export const parseStatus = (data) => {
 
     output.tags = data.tags
 
-    output.is_edited = data.edited_at !== null
+    output.edited_at = data.edited_at
 
     if (data.pleroma) {
       const { pleroma } = data
@@ -378,6 +378,10 @@ export const parseStatus = (data) => {
   output.favoritedBy = []
   output.rebloggedBy = []
 
+  if (data.hasOwnProperty('originalStatus')) {
+    Object.assign(output, data.originalStatus)
+  }
+
   return output
 }
 
-- 
cgit v1.2.3-70-g09d2


From f56298adbd6fc4b30f759f58336f881e9ebfab65 Mon Sep 17 00:00:00 2001
From: Sean King <seanking2919@protonmail.com>
Date: Tue, 21 Jun 2022 17:44:04 -0600
Subject: Add ability to detect whether backend supports editing

---
 src/App.js                                     | 1 +
 src/App.vue                                    | 4 ++--
 src/boot/after_store.js                        | 1 +
 src/components/extra_buttons/extra_buttons.js  | 3 ++-
 src/components/extra_buttons/extra_buttons.vue | 4 ++--
 5 files changed, 8 insertions(+), 5 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index af638a1c..f87b3962 100644
--- a/src/App.js
+++ b/src/App.js
@@ -94,6 +94,7 @@ export default {
       return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
     },
     showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
+    editingAvailable () { return this.$store.state.instance.editingAvailable },
     shoutboxPosition () {
       return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
     },
diff --git a/src/App.vue b/src/App.vue
index 1b513e08..c20a4d9d 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -52,8 +52,8 @@
     <MobilePostStatusButton />
     <UserReportingModal />
     <PostStatusModal />
-    <EditStatusModal />
-    <StatusHistoryModal />
+    <EditStatusModal v-if="editingAvailable" />
+    <StatusHistoryModal v-if="editingAvailable" />
     <SettingsModal />
     <div id="modal" />
     <GlobalNoticeList />
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index f655c38f..55e46c85 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -251,6 +251,7 @@ const getNodeInfo = async ({ store }) => {
       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: 'editingAvailable', value: features.includes('editing') })
       store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
       store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
 
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 11fe3235..92eb63b0 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -123,7 +123,8 @@ const ExtraButtons = {
     },
     isEdited () {
       return this.status.edited_at !== null
-    }
+    },
+    editingAvailable () { return this.$store.state.instance.editingAvailable }
   }
 }
 
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 877e1424..eda838dc 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -74,7 +74,7 @@
           /><span>{{ $t("status.unbookmark") }}</span>
         </button>
         <button
-          v-if="ownStatus"
+          v-if="ownStatus && editingAvailable"
           class="button-default dropdown-item dropdown-item-icon"
           @click.prevent="editStatus"
           @click="close"
@@ -85,7 +85,7 @@
           /><span>{{ $t("status.edit") }}</span>
         </button>
         <button
-          v-if="isEdited"
+          v-if="isEdited && editingAvailable"
           class="button-default dropdown-item dropdown-item-icon"
           @click.prevent="showStatusHistory"
           @click="close"
-- 
cgit v1.2.3-70-g09d2


From fddb531ed20b31c44e6911418e7bbb884836c40a Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 31 Jul 2022 12:35:48 +0300
Subject: --fix

---
 src/App.vue                                        | 20 ++++--
 src/boot/after_store.js                            |  4 +-
 src/boot/routes.js                                 |  9 ++-
 src/components/about/about.vue                     |  2 +-
 src/components/account_actions/account_actions.vue |  4 +-
 src/components/avatar_list/avatar_list.vue         |  2 +-
 src/components/basic_user_card/basic_user_card.vue | 11 ++--
 src/components/chat/chat.js                        |  2 +-
 src/components/chat_list/chat_list.vue             |  2 +-
 src/components/chat_message/chat_message.vue       | 10 +--
 src/components/chat_title/chat_title.vue           |  4 +-
 src/components/checkbox/checkbox.vue               |  4 +-
 src/components/color_input/color_input.vue         |  2 +-
 src/components/conversation/conversation.js        |  2 +-
 src/components/conversation/conversation.vue       |  2 +-
 src/components/desktop_nav/desktop_nav.js          | 24 +++++---
 .../domain_mute_card/domain_mute_card.vue          |  4 +-
 src/components/emoji_input/emoji_input.js          |  2 +-
 src/components/emoji_picker/emoji_picker.js        |  2 +-
 src/components/emoji_reactions/emoji_reactions.vue |  2 +-
 src/components/extra_buttons/extra_buttons.js      |  2 +-
 src/components/extra_buttons/extra_buttons.vue     |  6 +-
 src/components/favorite_button/favorite_button.vue |  2 +-
 src/components/features_panel/features_panel.vue   |  2 +-
 src/components/flash/flash.js                      |  2 +-
 src/components/font_control/font_control.vue       |  2 +-
 src/components/hashtag_link/hashtag_link.vue       |  4 +-
 src/components/image_cropper/image_cropper.js      |  2 +-
 .../instance_specific_panel.vue                    |  2 +-
 src/components/interactions/interactions.js        |  2 +-
 .../interface_language_switcher.vue                |  1 +
 src/components/login_form/login_form.js            |  2 +-
 src/components/login_form/login_form.vue           |  2 +-
 src/components/media_upload/media_upload.js        |  5 +-
 src/components/media_upload/media_upload.vue       |  2 +-
 src/components/mention_link/mention_link.vue       | 17 ++---
 src/components/mentions_line/mentions_line.vue     | 14 ++---
 src/components/mfa_form/recovery_form.vue          |  2 +-
 src/components/mobile_nav/mobile_nav.vue           |  5 +-
 src/components/modal/modal.vue                     |  8 +--
 .../moderation_tools/moderation_tools.vue          |  8 +--
 .../mrf_transparency_panel.js                      |  6 +-
 src/components/nav_panel/nav_panel.vue             |  2 +-
 src/components/notification/notification.js        |  2 +-
 src/components/notification/notification.vue       |  4 +-
 .../notifications/notification_filters.vue         |  4 +-
 src/components/notifications/notifications.vue     |  5 +-
 src/components/popover/popover.js                  | 34 +++++-----
 .../post_status_form/post_status_form.js           | 16 ++---
 src/components/react_button/react_button.js        |  2 +-
 src/components/react_button/react_button.vue       |  8 +--
 src/components/remote_follow/remote_follow.js      |  2 +-
 src/components/retweet_button/retweet_button.vue   |  2 +-
 src/components/search_bar/search_bar.js            |  2 +-
 src/components/selectable_list/selectable_list.vue |  4 +-
 .../settings_modal/helpers/modified_indicator.vue  |  4 +-
 .../helpers/server_side_indicator.vue              |  4 +-
 src/components/settings_modal/settings_modal.vue   |  4 +-
 src/components/settings_modal/tabs/general_tab.vue | 10 ++-
 .../settings_modal/tabs/mutes_and_blocks_tab.vue   | 34 +++++-----
 src/components/settings_modal/tabs/profile_tab.js  | 10 +--
 src/components/settings_modal/tabs/profile_tab.vue |  2 +-
 .../settings_modal/tabs/security_tab/mfa.js        |  6 +-
 .../settings_modal/tabs/security_tab/mfa_totp.js   |  2 +-
 .../tabs/security_tab/security_tab.js              |  2 +-
 .../settings_modal/tabs/theme_tab/preview.vue      |  5 +-
 .../settings_modal/tabs/theme_tab/theme_tab.js     |  8 +--
 src/components/shadow_control/shadow_control.js    |  8 ++-
 src/components/shadow_control/shadow_control.vue   |  2 +-
 src/components/shout_panel/shout_panel.js          |  2 +-
 src/components/side_drawer/side_drawer.js          |  2 +-
 src/components/side_drawer/side_drawer.vue         |  2 +-
 src/components/staff_panel/staff_panel.js          |  4 +-
 src/components/staff_panel/staff_panel.vue         |  2 +-
 src/components/status/status.js                    |  6 +-
 src/components/status/status.vue                   | 11 ++--
 src/components/status_body/status_body.vue         |  2 +-
 src/components/status_content/status_content.vue   |  2 +-
 src/components/status_popover/status_popover.vue   |  8 +--
 src/components/sticker_picker/sticker_picker.js    |  4 +-
 .../terms_of_service_panel.vue                     |  2 +-
 src/components/timeline/timeline.vue               |  5 +-
 .../timeline/timeline_quick_settings.vue           |  4 +-
 src/components/timeline_menu/timeline_menu.js      |  6 +-
 src/components/timeline_menu/timeline_menu.vue     |  6 +-
 .../timeline_menu/timeline_menu_content.vue        |  2 +-
 src/components/user_card/user_card.js              |  2 +-
 src/components/user_card/user_card.vue             |  2 +-
 .../user_list_popover/user_list_popover.vue        |  6 +-
 src/components/user_popover/user_popover.vue       | 42 ++++++-------
 src/components/user_profile/user_profile.vue       | 15 +++--
 .../user_reporting_modal/user_reporting_modal.vue  |  2 +-
 src/components/who_to_follow/who_to_follow.js      |  2 +-
 .../who_to_follow_panel/who_to_follow_panel.js     | 10 +--
 .../who_to_follow_panel/who_to_follow_panel.vue    |  2 +-
 src/i18n/messages.js                               |  2 +-
 src/lib/notification-i18n-loader.js                |  4 +-
 src/lib/persisted_state.js                         | 12 ++--
 src/modules/api.js                                 |  4 +-
 src/modules/config.js                              |  2 +-
 src/modules/errors.js                              |  4 +-
 src/modules/serverSideConfig.js                    | 28 ++++-----
 src/modules/statuses.js                            | 20 +++---
 src/modules/users.js                               | 24 ++++----
 src/services/api/api.service.js                    | 72 +++++++++++-----------
 src/services/chat_service/chat_service.js          | 12 ++--
 src/services/chat_utils/chat_utils.js              |  2 +-
 src/services/color_convert/color_convert.js        | 12 ++--
 src/services/completion/completion.js              |  2 +-
 src/services/date_utils/date_utils.js              |  2 +-
 .../entity_normalizer/entity_normalizer.service.js |  4 +-
 src/services/file_size_format/file_size_format.js  |  8 +--
 .../html_converter/html_line_converter.service.js  |  4 +-
 src/services/html_converter/utility.service.js     |  2 +-
 src/services/locale/locale.service.js              | 14 ++---
 src/services/new_api/password_reset.js             |  2 +-
 .../notifications_fetcher.service.js               | 12 ++--
 src/services/push/push.js                          |  4 +-
 src/services/theme_data/theme_data.service.js      |  2 +-
 .../timeline_fetcher/timeline_fetcher.service.js   | 16 ++---
 src/services/user_highlighter/user_highlighter.js  |  2 +-
 src/sw.js                                          |  4 +-
 test/e2e/custom-assertions/elementCount.js         |  2 +-
 test/e2e/nightwatch.conf.js                        | 54 ++++++++--------
 test/e2e/runner.js                                 |  8 +--
 test/unit/karma.conf.js                            | 14 ++---
 test/unit/specs/components/emoji_input.spec.js     |  2 +-
 test/unit/specs/components/rich_content.spec.js    |  2 +-
 test/unit/specs/components/user_profile.spec.js    |  6 +-
 test/unit/specs/modules/statuses.spec.js           |  4 +-
 .../services/chat_service/chat_service.spec.js     | 24 ++++----
 .../entity_normalizer/entity_normalizer.spec.js    |  8 +--
 .../file_size_format/file_size_format.spec.js      |  4 +-
 133 files changed, 508 insertions(+), 449 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 7d4a8e1e..0efadaf0 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -15,8 +15,12 @@
       class="app-layout container"
       :class="classes"
     >
-      <div class="underlay"/>
-      <div id="sidebar" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }">
+      <div class="underlay" />
+      <div
+        id="sidebar"
+        class="column -scrollable"
+        :class="{ '-show-scrollbar': showScrollbars }"
+      >
         <user-panel />
         <template v-if="layoutType !== 'mobile'">
           <nav-panel />
@@ -26,7 +30,11 @@
           <div id="notifs-sidebar" />
         </template>
       </div>
-      <div id="main-scroller" class="column main" :class="{ '-full-height': isChats }">
+      <div
+        id="main-scroller"
+        class="column main"
+        :class="{ '-full-height': isChats }"
+      >
         <div
           v-if="!currentUser"
           class="login-hint panel panel-default"
@@ -40,7 +48,11 @@
         </div>
         <router-view />
       </div>
-      <div id="notifs-column" class="column -scrollable" :class="{ '-show-scrollbar': showScrollbars }"/>
+      <div
+        id="notifs-column"
+        class="column -scrollable"
+        :class="{ '-show-scrollbar': showScrollbars }"
+      />
     </div>
     <MediaModal />
     <shout-panel
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 894a68e1..23b68e83 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -156,7 +156,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
   copyInstanceOption('hideSitename')
   copyInstanceOption('sidebarRight')
 
-  return store.dispatch('setTheme', config['theme'])
+  return store.dispatch('setTheme', config.theme)
 }
 
 const getTOS = async ({ store }) => {
@@ -197,7 +197,7 @@ const getStickers = async ({ store }) => {
       const stickers = (await Promise.all(
         Object.entries(values).map(async ([name, path]) => {
           const resPack = await window.fetch(path + 'pack.json')
-          var meta = {}
+          let meta = {}
           if (resPack.ok) {
             meta = await resPack.json()
           }
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 726476a8..c8194d5f 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -31,7 +31,8 @@ export default (store) => {
   }
 
   let routes = [
-    { name: 'root',
+    {
+      name: 'root',
       path: '/',
       redirect: _to => {
         return (store.state.users.currentUser
@@ -45,12 +46,14 @@ export default (store) => {
     { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
     { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
     { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
-    { name: 'remote-user-profile-acct',
+    {
+      name: 'remote-user-profile-acct',
       path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
       component: RemoteUserResolver,
       beforeEnter: validateAuthenticatedRoute
     },
-    { name: 'remote-user-profile',
+    {
+      name: 'remote-user-profile',
       path: '/remote-users/:hostname/:username',
       component: RemoteUserResolver,
       beforeEnter: validateAuthenticatedRoute
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index 5d5d6479..33586c97 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -8,7 +8,7 @@
   </div>
 </template>
 
-<script src="./about.js" ></script>
+<script src="./about.js"></script>
 
 <style lang="scss">
 </style>
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index c35d01af..23547f2c 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -6,7 +6,7 @@
       :bound-to="{ x: 'container' }"
       remove-padding
     >
-      <template v-slot:content>
+      <template #content>
         <div class="dropdown-menu">
           <template v-if="relationship.following">
             <button
@@ -57,7 +57,7 @@
           </button>
         </div>
       </template>
-      <template v-slot:trigger>
+      <template #trigger>
         <button class="button-unstyled ellipsis-button">
           <FAIcon
             class="icon"
diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue
index e1b6e971..9a6ca3f6 100644
--- a/src/components/avatar_list/avatar_list.vue
+++ b/src/components/avatar_list/avatar_list.vue
@@ -14,7 +14,7 @@
   </div>
 </template>
 
-<script src="./avatar_list.js" ></script>
+<script src="./avatar_list.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index effd9268..9cca7840 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -1,10 +1,13 @@
 <template>
   <div class="basic-user-card">
-    <router-link @click.prevent :to="userProfileLink(user)">
+    <router-link
+      :to="userProfileLink(user)"
+      @click.prevent
+    >
       <UserPopover
-        :userId="user.id"
-        :overlayCenters="true"
-        overlayCentersSelector=".avatar"
+        :user-id="user.id"
+        :overlay-centers="true"
+        overlay-centers-selector=".avatar"
       >
         <UserAvatar
           class="user-avatar avatar"
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 9f6e64e3..5a5c37b6 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -107,7 +107,7 @@ const Chat = {
         }
       })
     },
-    '$route': function () {
+    $route: function () {
       this.startFetching()
     },
     mastoUserSocketStatus (newValue) {
diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue
index 58e8d0b3..1248c4c8 100644
--- a/src/components/chat_list/chat_list.vue
+++ b/src/components/chat_list/chat_list.vue
@@ -23,7 +23,7 @@
         class="timeline"
       >
         <List :items="sortedChatList">
-          <template v-slot:item="{item}">
+          <template #item="{item}">
             <ChatListItem
               :key="item.id"
               :compact="false"
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index 9b823f7d..d635c47e 100644
--- a/src/components/chat_message/chat_message.vue
+++ b/src/components/chat_message/chat_message.vue
@@ -16,7 +16,7 @@
       >
         <UserPopover
           v-if="chatViewItem.isHead"
-          :userId="author.id"
+          :user-id="author.id"
         >
           <UserAvatar
             :compact="true"
@@ -50,7 +50,7 @@
                 @show="menuOpened = true"
                 @close="menuOpened = false"
               >
-                <template v-slot:content>
+                <template #content>
                   <div class="dropdown-menu">
                     <button
                       class="button-default dropdown-item dropdown-item-icon"
@@ -60,7 +60,7 @@
                     </button>
                   </div>
                 </template>
-                <template v-slot:trigger>
+                <template #trigger>
                   <button
                     class="button-default menu-icon"
                     :title="$t('chats.more')"
@@ -75,7 +75,7 @@
               :status="messageForStatusContent"
               :full-content="true"
             >
-              <template v-slot:footer>
+              <template #footer>
                 <span
                   class="created-at"
                 >
@@ -96,7 +96,7 @@
   </div>
 </template>
 
-<script src="./chat_message.js" ></script>
+<script src="./chat_message.js"></script>
 <style lang="scss">
 @import './chat_message.scss';
 
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index 22d0e7c4..ab7491fa 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -4,9 +4,9 @@
     :title="title"
   >
     <UserPopover
-      class="avatar-container"
       v-if="withAvatar && user"
-      :userId="user.id"
+      class="avatar-container"
+      :user-id="user.id"
     >
       <UserAvatar
         class="titlebar-avatar"
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 83695912..b6768d67 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -22,12 +22,12 @@
 
 <script>
 export default {
-  emits: ['update:modelValue'],
   props: [
     'modelValue',
     'indeterminate',
     'disabled'
-  ]
+  ],
+  emits: ['update:modelValue']
 }
 </script>
 
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index e84603c3..dfc084f9 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -46,7 +46,6 @@
     </div>
   </div>
 </template>
-<style lang="scss" src="./color_input.scss"></style>
 <script>
 import Checkbox from '../checkbox/checkbox.vue'
 import { hex2rgb } from '../../services/color_convert/color_convert.js'
@@ -108,6 +107,7 @@ export default {
   }
 }
 </script>
+<style lang="scss" src="./color_input.scss"></style>
 
 <style lang="scss">
 .color-control {
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 2ef2977a..3b540cac 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -271,7 +271,7 @@ const conversation = {
           result[irid] = result[irid] || []
           result[irid].push({
             name: `#${i}`,
-            id: id
+            id
           })
         }
         i++
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 6088e1ca..1adbe250 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -31,8 +31,8 @@
             keypath="status.show_all_conversation_with_icon"
             tag="button"
             class="button-unstyled -link"
-            @click.prevent="diveToTopLevel"
             scope="global"
+            @click.prevent="diveToTopLevel"
           >
             <template #icon>
               <FAIcon
diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js
index e048f53d..08c0e44e 100644
--- a/src/components/desktop_nav/desktop_nav.js
+++ b/src/components/desktop_nav/desktop_nav.js
@@ -46,23 +46,27 @@ export default {
     enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
     logoStyle () {
       return {
-        'visibility': this.enableMask ? 'hidden' : 'visible'
+        visibility: this.enableMask ? 'hidden' : 'visible'
       }
     },
     logoMaskStyle () {
-      return this.enableMask ? {
-        'mask-image': `url(${this.$store.state.instance.logo})`
-      } : {
-        'background-color': this.enableMask ? '' : 'transparent'
-      }
+      return this.enableMask
+        ? {
+            'mask-image': `url(${this.$store.state.instance.logo})`
+          }
+        : {
+            'background-color': this.enableMask ? '' : 'transparent'
+          }
     },
     logoBgStyle () {
       return Object.assign({
-        'margin': `${this.$store.state.instance.logoMargin} 0`,
+        margin: `${this.$store.state.instance.logoMargin} 0`,
         opacity: this.searchBarHidden ? 1 : 0
-      }, this.enableMask ? {} : {
-        'background-color': this.enableMask ? '' : 'transparent'
-      })
+      }, this.enableMask
+        ? {}
+        : {
+            'background-color': this.enableMask ? '' : 'transparent'
+          })
     },
     logo () { return this.$store.state.instance.logo },
     sitename () { return this.$store.state.instance.name },
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
index 836688aa..28c61631 100644
--- a/src/components/domain_mute_card/domain_mute_card.vue
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -9,7 +9,7 @@
       class="btn button-default"
     >
       {{ $t('domain_mute_card.unmute') }}
-      <template v-slot:progress>
+      <template #progress>
         {{ $t('domain_mute_card.unmute_progress') }}
       </template>
     </ProgressButton>
@@ -19,7 +19,7 @@
       class="btn button-default"
     >
       {{ $t('domain_mute_card.mute') }}
-      <template v-slot:progress>
+      <template #progress>
         {{ $t('domain_mute_card.mute_progress') }}
       </template>
     </ProgressButton>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 391cc5b5..5ba3907f 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -321,7 +321,7 @@ const EmojiInput = {
       }
     },
     scrollIntoView () {
-      const rootRef = this.$refs['picker'].$el
+      const rootRef = this.$refs.picker.$el
       /* Scroller is either `window` (replies in TL), sidebar (main post form,
        * replies in notifs) or mobile post form. Note that getting and setting
        * scroll is different for `Window` and `Element`s
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index bd5c2e39..f6920208 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -25,7 +25,7 @@ const filterByKeyword = (list, keyword = '') => {
   if (keyword === '') return list
 
   const keywordLowercase = keyword.toLowerCase()
-  let orderedEmojiList = []
+  const orderedEmojiList = []
   for (const emoji of list) {
     const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
     if (indexOfKeyword > -1) {
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 51d50359..4ea8b6a2 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -26,7 +26,7 @@
   </div>
 </template>
 
-<script src="./emoji_reactions.js" ></script>
+<script src="./emoji_reactions.js"></script>
 <style lang="scss">
 @import '../../_variables.scss';
 
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 2f534896..22ffb65a 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -25,7 +25,7 @@ library.add(
 )
 
 const ExtraButtons = {
-  props: [ 'status' ],
+  props: ['status'],
   components: { Popover },
   methods: {
     deleteStatus () {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 2ea86201..2c893bf3 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -7,7 +7,7 @@
     :bound-to="{ x: 'container' }"
     remove-padding
   >
-    <template v-slot:content="{close}">
+    <template #content="{close}">
       <div class="dropdown-menu">
         <button
           v-if="canMute && !status.thread_muted"
@@ -120,7 +120,7 @@
         </button>
       </div>
     </template>
-    <template v-slot:trigger>
+    <template #trigger>
       <span class="button-unstyled popover-trigger">
         <FAIcon
           class="fa-scale-110 fa-old-padding"
@@ -131,7 +131,7 @@
   </Popover>
 </template>
 
-<script src="./extra_buttons.js" ></script>
+<script src="./extra_buttons.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index dce25e24..d5c4c61e 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -29,7 +29,7 @@
   </div>
 </template>
 
-<script src="./favorite_button.js" ></script>
+<script src="./favorite_button.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue
index a58a99af..4cdf56d0 100644
--- a/src/components/features_panel/features_panel.vue
+++ b/src/components/features_panel/features_panel.vue
@@ -32,7 +32,7 @@
   </div>
 </template>
 
-<script src="./features_panel.js" ></script>
+<script src="./features_panel.js"></script>
 
 <style lang="scss">
   .features-panel li {
diff --git a/src/components/flash/flash.js b/src/components/flash/flash.js
index 87f940a7..87c1d650 100644
--- a/src/components/flash/flash.js
+++ b/src/components/flash/flash.js
@@ -11,7 +11,7 @@ library.add(
 )
 
 const Flash = {
-  props: [ 'src' ],
+  props: ['src'],
   data () {
     return {
       player: false, // can be true, "hidden", false. hidden = element exists
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
index f100c3a9..83c1cef7 100644
--- a/src/components/font_control/font_control.vue
+++ b/src/components/font_control/font_control.vue
@@ -47,7 +47,7 @@
   </div>
 </template>
 
-<script src="./font_control.js" ></script>
+<script src="./font_control.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/hashtag_link/hashtag_link.vue b/src/components/hashtag_link/hashtag_link.vue
index 918ed26b..596851b9 100644
--- a/src/components/hashtag_link/hashtag_link.vue
+++ b/src/components/hashtag_link/hashtag_link.vue
@@ -14,6 +14,6 @@
   </span>
 </template>
 
-<script src="./hashtag_link.js"/>
+<script src="./hashtag_link.js" />
 
-<style lang="scss" src="./hashtag_link.scss"/>
+<style lang="scss" src="./hashtag_link.scss" />
diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index 05f6fd4c..55e901a0 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -95,7 +95,7 @@ const ImageCropper = {
       const fileInput = this.$refs.input
       if (fileInput.files != null && fileInput.files[0] != null) {
         this.file = fileInput.files[0]
-        let reader = new window.FileReader()
+        const reader = new window.FileReader()
         reader.onload = (e) => {
           this.dataUrl = e.target.result
           this.$emit('open')
diff --git a/src/components/instance_specific_panel/instance_specific_panel.vue b/src/components/instance_specific_panel/instance_specific_panel.vue
index 7448ca06..c8ed0a2d 100644
--- a/src/components/instance_specific_panel/instance_specific_panel.vue
+++ b/src/components/instance_specific_panel/instance_specific_panel.vue
@@ -10,4 +10,4 @@
   </div>
 </template>
 
-<script src="./instance_specific_panel.js" ></script>
+<script src="./instance_specific_panel.js"></script>
diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js
index c5ceb63d..cc6a15e1 100644
--- a/src/components/interactions/interactions.js
+++ b/src/components/interactions/interactions.js
@@ -12,7 +12,7 @@ const Interactions = {
   data () {
     return {
       allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
-      filterMode: tabModeDict['mentions']
+      filterMode: tabModeDict.mentions
     }
   },
   methods: {
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 7ad1fe2e..6997f149 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -25,6 +25,7 @@ import Select from '../select/select.vue'
 
 export default {
   components: {
+    // eslint-disable-next-line vue/no-reserved-component-names
     Select
   },
   props: {
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 638bd812..b795640e 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -83,7 +83,7 @@ const LoginForm = {
     },
     clearError () { this.error = false },
     focusOnPasswordInput () {
-      let passwordInput = this.$refs.passwordInput
+      const passwordInput = this.$refs.passwordInput
       passwordInput.focus()
       passwordInput.setSelectionRange(0, passwordInput.value.length)
     }
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 21482977..7a430c51 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -90,7 +90,7 @@
   </div>
 </template>
 
-<script src="./login_form.js" ></script>
+<script src="./login_form.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 669d8190..cfd42d4c 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -42,7 +42,8 @@ const mediaUpload = {
         .then((fileData) => {
           self.$emit('uploaded', fileData)
           self.decreaseUploadCount()
-        }, (error) => { // eslint-disable-line handle-callback-err
+        }, (error) => {
+          console.error('Error uploading file', error)
           self.$emit('upload-failed', 'default')
           self.decreaseUploadCount()
         })
@@ -73,7 +74,7 @@ const mediaUpload = {
     'disabled'
   ],
   watch: {
-    'dropFiles': function (fileInfos) {
+    dropFiles: function (fileInfos) {
       if (!this.uploading) {
         this.multiUpload(fileInfos)
       }
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 7cc59f5a..a538a5ed 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -26,7 +26,7 @@
   </label>
 </template>
 
-<script src="./media_upload.js" ></script>
+<script src="./media_upload.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 56da8e0f..3af502ef 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -12,7 +12,7 @@
     /><!-- eslint-enable vue/no-v-html -->
     <UserPopover
       v-else
-      :userId="user.id"
+      :user-id="user.id"
       :disabled="!shouldShowTooltip"
     >
       <span
@@ -54,16 +54,19 @@
             :class="{ '-you': shouldBoldenYou }"
           > {{ ' ' + $t('status.you') }}</span>
           <!-- eslint-enable vue/no-v-html -->
-        </a><span class="full" ref="full">
-            <!-- eslint-disable vue/no-v-html -->
-            @<span v-html="userName" /><span v-html="'@' + serverName" />
-            <!-- eslint-enable vue/no-v-html -->
+        </a><span
+          ref="full"
+          class="full"
+        >
+          <!-- eslint-disable vue/no-v-html -->
+          @<span v-html="userName" /><span v-html="'@' + serverName" />
+          <!-- eslint-enable vue/no-v-html -->
         </span>
       </span>
     </UserPopover>
   </span>
 </template>
 
-<script src="./mention_link.js"/>
+<script src="./mention_link.js" />
 
-<style lang="scss" src="./mention_link.scss"/>
+<style lang="scss" src="./mention_link.scss" />
diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue
index 161855e8..64c19bf1 100644
--- a/src/components/mentions_line/mentions_line.vue
+++ b/src/components/mentions_line/mentions_line.vue
@@ -14,12 +14,12 @@
         v-if="expanded"
         class="fullExtraMentions"
       >{{ ' ' }}<MentionLink
-          v-for="mention in extraMentions"
-          :key="mention.index"
-          class="mention-link"
-          :content="mention.content"
-          :url="mention.url"
-        />
+        v-for="mention in extraMentions"
+        :key="mention.index"
+        class="mention-link"
+        :content="mention.content"
+        :url="mention.url"
+      />
       </span><button
         v-if="!expanded"
         class="button-unstyled showMoreLess"
@@ -36,5 +36,5 @@
     </span>
   </span>
 </template>
-<script src="./mentions_line.js" ></script>
+<script src="./mentions_line.js"></script>
 <style lang="scss" src="./mentions_line.scss" />
diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue
index a9cf39aa..5988fa51 100644
--- a/src/components/mfa_form/recovery_form.vue
+++ b/src/components/mfa_form/recovery_form.vue
@@ -69,4 +69,4 @@
     </div>
   </div>
 </template>
-<script src="./recovery_form.js" ></script>
+<script src="./recovery_form.js"></script>
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index c58d9e7c..949cf17e 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -67,11 +67,10 @@
         </a>
       </div>
       <div
-        class="mobile-notifications"
         id="mobile-notifications"
+        class="mobile-notifications"
         @scroll="onScroll"
-      >
-      </div>
+      />
     </div>
     <SideDrawer
       ref="sideDrawer"
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index 52d8d27e..2187f392 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -12,6 +12,9 @@
 
 <script>
 export default {
+  provide: {
+    popoversZLayer: 'modals'
+  },
   props: {
     isOpen: {
       type: Boolean,
@@ -22,14 +25,11 @@ export default {
       default: false
     }
   },
-  provide: {
-    popoversZLayer: 'modals'
-  },
   computed: {
     classes () {
       return {
         'modal-background': !this.noBackground,
-        'open': this.isOpen
+        open: this.isOpen
       }
     }
   }
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index 96b8c3a3..34fe2e7c 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -8,7 +8,7 @@
       @show="setToggled(true)"
       @close="setToggled(false)"
     >
-      <template v-slot:content>
+      <template #content>
         <div class="dropdown-menu">
           <span v-if="user.is_local">
             <button
@@ -122,7 +122,7 @@
           </span>
         </div>
       </template>
-      <template v-slot:trigger>
+      <template #trigger>
         <button
           class="btn button-default btn-block moderation-tools-button"
           :class="{ toggled }"
@@ -137,11 +137,11 @@
         v-if="showDeleteUserDialog"
         :on-cancel="deleteUserDialog.bind(this, false)"
       >
-        <template v-slot:header>
+        <template #header>
           {{ $t('user_card.admin_menu.delete_user') }}
         </template>
         <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
-        <template v-slot:footer>
+        <template #footer>
           <button
             class="btn button-default"
             @click="deleteUserDialog(false)"
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
index 3fde8106..13cfb52e 100644
--- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js
+++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
@@ -9,10 +9,10 @@ import { get } from 'lodash'
  */
 const toInstanceReasonObject = (instances, info, key) => {
   return instances.map(instance => {
-    if (info[key] && info[key][instance] && info[key][instance]['reason']) {
-      return { instance: instance, reason: info[key][instance]['reason'] }
+    if (info[key] && info[key][instance] && info[key][instance].reason) {
+      return { instance, reason: info[key][instance].reason }
     }
-    return { instance: instance, reason: '' }
+    return { instance, reason: '' }
   })
 }
 
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 62aab7f1..3fd27d89 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -90,7 +90,7 @@
   </div>
 </template>
 
-<script src="./nav_panel.js" ></script>
+<script src="./nav_panel.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 77cdfa73..882b68f9 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -40,7 +40,7 @@ const Notification = {
       unmuted: false
     }
   },
-  props: [ 'notification' ],
+  props: ['notification'],
   components: {
     StatusContent,
     UserAvatar,
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 6427d117..d2b903f6 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -37,8 +37,8 @@
         @click.prevent
       >
         <UserPopover
-          :userId="notification.from_profile.id"
-          :overlayCenters="true"
+          :user-id="notification.from_profile.id"
+          :overlay-centers="true"
         >
           <UserAvatar
             class="post-avatar"
diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue
index 00a531b3..b0213167 100644
--- a/src/components/notifications/notification_filters.vue
+++ b/src/components/notifications/notification_filters.vue
@@ -5,7 +5,7 @@
     placement="bottom"
     :bound-to="{ x: 'container' }"
   >
-    <template v-slot:content>
+    <template #content>
       <div class="dropdown-menu">
         <button
           class="button-default dropdown-item"
@@ -72,7 +72,7 @@
         </button>
       </div>
     </template>
-    <template v-slot:trigger>
+    <template #trigger>
       <button class="filter-trigger-button button-unstyled">
         <FAIcon icon="filter" />
       </button>
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index b46c06aa..e778e27b 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -1,5 +1,8 @@
 <template>
-  <teleport :disabled="minimalMode || disableTeleport" :to="teleportTarget">
+  <teleport
+    :disabled="minimalMode || disableTeleport"
+    :to="teleportTarget"
+  >
     <div
       :class="{ minimal: minimalMode }"
       class="Notifications"
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 58f7126d..d2af59fe 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -104,21 +104,25 @@ const Popover = {
 
       // What are the screen bounds for the popover? Viewport vs container
       // when using viewport, using default margin values to dodge the navbar
-      const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
-        min: parentScreenBox.left + (margin.left || 0),
-        max: parentScreenBox.right - (margin.right || 0)
-      } : {
-        min: 0 + (margin.left || 10),
-        max: window.innerWidth - (margin.right || 10)
-      }
-
-      const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
-        min: parentScreenBox.top + (margin.top || 0),
-        max: parentScreenBox.bottom - (margin.bottom || 0)
-      } : {
-        min: 0 + (margin.top || 50),
-        max: window.innerHeight - (margin.bottom || 5)
-      }
+      const xBounds = this.boundTo && this.boundTo.x === 'container'
+        ? {
+            min: parentScreenBox.left + (margin.left || 0),
+            max: parentScreenBox.right - (margin.right || 0)
+          }
+        : {
+            min: 0 + (margin.left || 10),
+            max: window.innerWidth - (margin.right || 10)
+          }
+
+      const yBounds = this.boundTo && this.boundTo.y === 'container'
+        ? {
+            min: parentScreenBox.top + (margin.top || 0),
+            max: parentScreenBox.bottom - (margin.bottom || 0)
+          }
+        : {
+            min: 0 + (margin.top || 50),
+            max: window.innerHeight - (margin.bottom || 5)
+          }
 
       let horizOffset = 0
       let vertOffset = 0
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 2febf226..c0d80b20 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -41,7 +41,7 @@ const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
   allAttentions = uniqBy(allAttentions, 'id')
   allAttentions = reject(allAttentions, { id: currentUser.id })
 
-  let mentions = map(allAttentions, (attention) => {
+  const mentions = map(allAttentions, (attention) => {
     return `@${attention.screen_name}`
   })
 
@@ -242,7 +242,7 @@ const PostStatusForm = {
     })
   },
   watch: {
-    'newStatus': {
+    newStatus: {
       deep: true,
       handler () {
         this.statusChanged()
@@ -273,7 +273,7 @@ const PostStatusForm = {
           this.$refs.textarea.focus()
         })
       }
-      let el = this.$el.querySelector('textarea')
+      const el = this.$el.querySelector('textarea')
       el.style.height = 'auto'
       el.style.height = undefined
       this.error = null
@@ -392,7 +392,7 @@ const PostStatusForm = {
       this.$emit('resize', { delayed: true })
     },
     removeMediaFile (fileInfo) {
-      let index = this.newStatus.files.indexOf(fileInfo)
+      const index = this.newStatus.files.indexOf(fileInfo)
       this.newStatus.files.splice(index, 1)
       this.$emit('resize')
     },
@@ -462,7 +462,7 @@ const PostStatusForm = {
     },
     onEmojiInputInput (e) {
       this.$nextTick(() => {
-        this.resize(this.$refs['textarea'])
+        this.resize(this.$refs.textarea)
       })
     },
     resize (e) {
@@ -477,8 +477,8 @@ const PostStatusForm = {
         return
       }
 
-      const formRef = this.$refs['form']
-      const bottomRef = this.$refs['bottom']
+      const formRef = this.$refs.form
+      const bottomRef = this.$refs.bottom
       /* Scroller is either `window` (replies in TL), sidebar (main post form,
        * replies in notifs) or mobile post form. Note that getting and setting
        * scroll is different for `Window` and `Element`s
@@ -564,7 +564,7 @@ const PostStatusForm = {
       this.$refs['emoji-input'].resize()
     },
     showEmojiPicker () {
-      this.$refs['textarea'].focus()
+      this.$refs.textarea.focus()
       this.$refs['emoji-input'].triggerShowPicker()
     },
     clearError () {
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index e6f9dbff..37d6e7d0 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -45,7 +45,7 @@ const ReactButton = {
     emojis () {
       if (this.filterWord !== '') {
         const filterWordLowercase = trim(this.filterWord.toLowerCase())
-        let orderedEmojiList = []
+        const orderedEmojiList = []
         for (const emoji of this.$store.state.instance.emoji) {
           if (emoji.replacement === this.filterWord) return [emoji]
 
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index b76fb244..5a809847 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -9,13 +9,13 @@
     popover-class="ReactButton popover-default"
     @show="focusInput"
   >
-    <template v-slot:content="{close}">
+    <template #content="{close}">
       <div class="reaction-picker-filter">
         <input
           v-model="filterWord"
-          @input="$event.target.composing = false"
           size="1"
           :placeholder="$t('emoji.search_emoji')"
+          @input="$event.target.composing = false"
         >
       </div>
       <div class="reaction-picker">
@@ -41,7 +41,7 @@
         <div class="reaction-bottom-fader" />
       </div>
     </template>
-    <template v-slot:trigger>
+    <template #trigger>
       <span
         class="button-unstyled popover-trigger"
         :title="$t('tool_tip.add_reaction')"
@@ -55,7 +55,7 @@
   </Popover>
 </template>
 
-<script src="./react_button.js" ></script>
+<script src="./react_button.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/remote_follow/remote_follow.js b/src/components/remote_follow/remote_follow.js
index 461d58c9..56b264fc 100644
--- a/src/components/remote_follow/remote_follow.js
+++ b/src/components/remote_follow/remote_follow.js
@@ -1,5 +1,5 @@
 export default {
-  props: [ 'user' ],
+  props: ['user'],
   computed: {
     subscribeUrl () {
       // eslint-disable-next-line no-undef
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 859ce499..5a15d387 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -36,7 +36,7 @@
   </div>
 </template>
 
-<script src="./retweet_button.js" ></script>
+<script src="./retweet_button.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js
index 551649c7..3b297f09 100644
--- a/src/components/search_bar/search_bar.js
+++ b/src/components/search_bar/search_bar.js
@@ -16,7 +16,7 @@ const SearchBar = {
     error: false
   }),
   watch: {
-    '$route': function (route) {
+    $route: function (route) {
       if (route.name === 'search') {
         this.searchTerm = route.query.query
       }
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index 3c80660e..1f7683ab 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -24,7 +24,7 @@
       :items="items"
       :get-key="getKey"
     >
-      <template v-slot:item="{item}">
+      <template #item="{item}">
         <div
           class="selectable-list-item-inner"
           :class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
@@ -41,7 +41,7 @@
           />
         </div>
       </template>
-      <template v-slot:empty>
+      <template #empty>
         <slot name="empty" />
       </template>
     </List>
diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue
index 74366dc1..8311533a 100644
--- a/src/components/settings_modal/helpers/modified_indicator.vue
+++ b/src/components/settings_modal/helpers/modified_indicator.vue
@@ -6,14 +6,14 @@
     <Popover
       trigger="hover"
     >
-      <template v-slot:trigger>
+      <template #trigger>
         &nbsp;
         <FAIcon
           icon="wrench"
           :aria-label="$t('settings.setting_changed')"
         />
       </template>
-      <template v-slot:content>
+      <template #content>
         <div class="modified-tooltip">
           {{ $t('settings.setting_changed') }}
         </div>
diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/server_side_indicator.vue
index cf53eb6a..bf181959 100644
--- a/src/components/settings_modal/helpers/server_side_indicator.vue
+++ b/src/components/settings_modal/helpers/server_side_indicator.vue
@@ -6,14 +6,14 @@
     <Popover
       trigger="hover"
     >
-      <template v-slot:trigger>
+      <template #trigger>
         &nbsp;
         <FAIcon
           icon="server"
           :aria-label="$t('settings.setting_server_side')"
         />
       </template>
-      <template v-slot:content>
+      <template #content>
         <div class="serverside-tooltip">
           {{ $t('settings.setting_server_side') }}
         </div>
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index d3bed061..7b457371 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -53,7 +53,7 @@
           :bound-to="{ x: 'container' }"
           remove-padding
         >
-          <template v-slot:trigger>
+          <template #trigger>
             <button
               class="btn button-default"
               :title="$t('general.close')"
@@ -65,7 +65,7 @@
               />
             </button>
           </template>
-          <template v-slot:content="{close}">
+          <template #content="{close}">
             <div class="dropdown-menu">
               <button
                 class="button-default dropdown-item dropdown-item-icon"
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index e201ab9d..a2609200 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -75,12 +75,18 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="userPopoverZoom" expert="1">
+          <BooleanSetting
+            path="userPopoverZoom"
+            expert="1"
+          >
             {{ $t('settings.user_popover_avatar_zoom') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="userPopoverOverlay" expert="1">
+          <BooleanSetting
+            path="userPopoverOverlay"
+            expert="1"
+          >
             {{ $t('settings.user_popover_avatar_overlay') }}
           </BooleanSetting>
         </li>
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
index 32a21415..c515d542 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -10,7 +10,7 @@
           :query="queryUserIds"
           :placeholder="$t('settings.search_user_to_block')"
         >
-          <template v-slot="row">
+          <template #default="row">
             <BlockCard
               :user-id="row.item"
             />
@@ -21,7 +21,7 @@
         :refresh="true"
         :get-key="i => i"
       >
-        <template v-slot:header="{selected}">
+        <template #header="{selected}">
           <div class="bulk-actions">
             <ProgressButton
               v-if="selected.length > 0"
@@ -29,7 +29,7 @@
               :click="() => blockUsers(selected)"
             >
               {{ $t('user_card.block') }}
-              <template v-slot:progress>
+              <template #progress>
                 {{ $t('user_card.block_progress') }}
               </template>
             </ProgressButton>
@@ -39,16 +39,16 @@
               :click="() => unblockUsers(selected)"
             >
               {{ $t('user_card.unblock') }}
-              <template v-slot:progress>
+              <template #progress>
                 {{ $t('user_card.unblock_progress') }}
               </template>
             </ProgressButton>
           </div>
         </template>
-        <template v-slot:item="{item}">
+        <template #item="{item}">
           <BlockCard :user-id="item" />
         </template>
-        <template v-slot:empty>
+        <template #empty>
           {{ $t('settings.no_blocks') }}
         </template>
       </BlockList>
@@ -63,7 +63,7 @@
               :query="queryUserIds"
               :placeholder="$t('settings.search_user_to_mute')"
             >
-              <template v-slot="row">
+              <template #default="row">
                 <MuteCard
                   :user-id="row.item"
                 />
@@ -74,7 +74,7 @@
             :refresh="true"
             :get-key="i => i"
           >
-            <template v-slot:header="{selected}">
+            <template #header="{selected}">
               <div class="bulk-actions">
                 <ProgressButton
                   v-if="selected.length > 0"
@@ -82,7 +82,7 @@
                   :click="() => muteUsers(selected)"
                 >
                   {{ $t('user_card.mute') }}
-                  <template v-slot:progress>
+                  <template #progress>
                     {{ $t('user_card.mute_progress') }}
                   </template>
                 </ProgressButton>
@@ -92,16 +92,16 @@
                   :click="() => unmuteUsers(selected)"
                 >
                   {{ $t('user_card.unmute') }}
-                  <template v-slot:progress>
+                  <template #progress>
                     {{ $t('user_card.unmute_progress') }}
                   </template>
                 </ProgressButton>
               </div>
             </template>
-            <template v-slot:item="{item}">
+            <template #item="{item}">
               <MuteCard :user-id="item" />
             </template>
-            <template v-slot:empty>
+            <template #empty>
               {{ $t('settings.no_mutes') }}
             </template>
           </MuteList>
@@ -114,7 +114,7 @@
               :query="queryKnownDomains"
               :placeholder="$t('settings.type_domains_to_mute')"
             >
-              <template v-slot="row">
+              <template #default="row">
                 <DomainMuteCard
                   :domain="row.item"
                 />
@@ -125,7 +125,7 @@
             :refresh="true"
             :get-key="i => i"
           >
-            <template v-slot:header="{selected}">
+            <template #header="{selected}">
               <div class="bulk-actions">
                 <ProgressButton
                   v-if="selected.length > 0"
@@ -133,16 +133,16 @@
                   :click="() => unmuteDomains(selected)"
                 >
                   {{ $t('domain_mute_card.unmute') }}
-                  <template v-slot:progress>
+                  <template #progress>
                     {{ $t('domain_mute_card.unmute_progress') }}
                   </template>
                 </ProgressButton>
               </div>
             </template>
-            <template v-slot:item="{item}">
+            <template #item="{item}">
               <DomainMuteCard :domain="item" />
             </template>
-            <template v-slot:empty>
+            <template #empty>
               {{ $t('settings.no_mutes') }}
             </template>
           </DomainMuteList>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 8781bb91..376248ef 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -71,10 +71,12 @@ const ProfileTab = {
       })
     },
     emojiSuggestor () {
-      return suggestor({ emoji: [
-        ...this.$store.state.instance.emoji,
-        ...this.$store.state.instance.customEmoji
-      ] })
+      return suggestor({
+        emoji: [
+          ...this.$store.state.instance.emoji,
+          ...this.$store.state.instance.customEmoji
+        ]
+      })
     },
     userSuggestor () {
       return suggestor({ store: this.$store })
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 4cd93772..642d54ca 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -117,8 +117,8 @@
         <button
           v-if="!isDefaultAvatar && pickAvatarBtnVisible"
           :title="$t('settings.reset_avatar')"
-          @click="resetAvatar"
           class="button-unstyled reset-button"
+          @click="resetAvatar"
         >
           <FAIcon
             icon="times"
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js
index abf37062..5337d150 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa.js
+++ b/src/components/settings_modal/tabs/security_tab/mfa.js
@@ -32,8 +32,8 @@ const Mfa = {
   components: {
     'recovery-codes': RecoveryCodes,
     'totp-item': TOTP,
-    'qrcode': VueQrcode,
-    'confirm': Confirm
+    qrcode: VueQrcode,
+    confirm: Confirm
   },
   computed: {
     canSetupOTP () {
@@ -139,7 +139,7 @@ const Mfa = {
 
     // fetch settings from server
     async fetchSettings () {
-      let result = await this.backendInteractor.settingsMFA()
+      const result = await this.backendInteractor.settingsMFA()
       if (result.error) return
       this.settings = result.settings
       this.settings.available = true
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
index 8408d8e9..b0adb530 100644
--- a/src/components/settings_modal/tabs/security_tab/mfa_totp.js
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
@@ -10,7 +10,7 @@ export default {
     inProgress: false // progress peform request to disable otp method
   }),
   components: {
-    'confirm': Confirm
+    confirm: Confirm
   },
   computed: {
     isActivated () {
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js
index fc732936..d253bc79 100644
--- a/src/components/settings_modal/tabs/security_tab/security_tab.js
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.js
@@ -13,7 +13,7 @@ const SecurityTab = {
       deletingAccount: false,
       deleteAccountConfirmPasswordInput: '',
       deleteAccountError: false,
-      changePasswordInputs: [ '', '', '' ],
+      changePasswordInputs: ['', '', ''],
       changedPassword: false,
       changePasswordError: false,
       moveAccountTarget: '',
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
index f266b603..ba6bd529 100644
--- a/src/components/settings_modal/tabs/theme_tab/preview.vue
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -29,7 +29,10 @@
               {{ $t('settings.style.preview.content') }}
             </h4>
 
-            <i18n-t scope="global" keypath="settings.style.preview.text">
+            <i18n-t
+              scope="global"
+              keypath="settings.style.preview.text"
+            >
               <code style="font-family: var(--postCodeFont)">
                 {{ $t('settings.style.preview.mono') }}
               </code>
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 7e1da7ab..282cb384 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -95,11 +95,11 @@ export default {
 
       ...Object.keys(SLOT_INHERITANCE)
         .map(key => [key, ''])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
+        .reduce((acc, [key, val]) => ({ ...acc, [key + 'ColorLocal']: val }), {}),
 
       ...Object.keys(OPACITIES)
         .map(key => [key, ''])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
+        .reduce((acc, [key, val]) => ({ ...acc, [key + 'OpacityLocal']: val }), {}),
 
       shadowSelected: undefined,
       shadowsLocal: {},
@@ -212,12 +212,12 @@ export default {
     currentColors () {
       return Object.keys(SLOT_INHERITANCE)
         .map(key => [key, this[key + 'ColorLocal']])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
+        .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
     },
     currentOpacity () {
       return Object.keys(OPACITIES)
         .map(key => [key, this[key + 'OpacityLocal']])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
+        .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
     },
     currentRadii () {
       return {
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
index 386a5756..a1d1012b 100644
--- a/src/components/shadow_control/shadow_control.js
+++ b/src/components/shadow_control/shadow_control.js
@@ -112,9 +112,11 @@ export default {
       return hex2rgb(this.selected.color)
     },
     style () {
-      return this.ready ? {
-        boxShadow: getCssShadow(this.fallback)
-      } : {}
+      return this.ready
+        ? {
+            boxShadow: getCssShadow(this.fallback)
+          }
+        : {}
     }
   }
 }
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index f2fc7b99..669cac71 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -215,7 +215,7 @@
   </div>
 </template>
 
-<script src="./shadow_control.js" ></script>
+<script src="./shadow_control.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/shout_panel/shout_panel.js b/src/components/shout_panel/shout_panel.js
index a6168971..fb0c5aa2 100644
--- a/src/components/shout_panel/shout_panel.js
+++ b/src/components/shout_panel/shout_panel.js
@@ -11,7 +11,7 @@ library.add(
 )
 
 const shoutPanel = {
-  props: [ 'floating' ],
+  props: ['floating'],
   data () {
     return {
       currentMessage: '',
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index bad1806b..f45f8def 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -32,7 +32,7 @@ library.add(
 )
 
 const SideDrawer = {
-  props: [ 'logout' ],
+  props: ['logout'],
   data: () => ({
     closed: true,
     closeGesture: undefined
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index aefd3915..7547fb08 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -204,7 +204,7 @@
   </div>
 </template>
 
-<script src="./side_drawer.js" ></script>
+<script src="./side_drawer.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js
index b9561bf1..a7fbc718 100644
--- a/src/components/staff_panel/staff_panel.js
+++ b/src/components/staff_panel/staff_panel.js
@@ -17,8 +17,8 @@ const StaffPanel = {
       const groupedStaffAccounts = groupBy(staffAccounts, 'role')
 
       return [
-        { role: 'admin', users: groupedStaffAccounts['admin'] },
-        { role: 'moderator', users: groupedStaffAccounts['moderator'] }
+        { role: 'admin', users: groupedStaffAccounts.admin },
+        { role: 'moderator', users: groupedStaffAccounts.moderator }
       ].filter(group => group.users)
     },
     ...mapGetters([
diff --git a/src/components/staff_panel/staff_panel.vue b/src/components/staff_panel/staff_panel.vue
index c52ade42..6b9e61f2 100644
--- a/src/components/staff_panel/staff_panel.vue
+++ b/src/components/staff_panel/staff_panel.vue
@@ -24,7 +24,7 @@
   </div>
 </template>
 
-<script src="./staff_panel.js" ></script>
+<script src="./staff_panel.js"></script>
 
 <style lang="scss">
 
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 053c9441..0a2690f6 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -448,7 +448,7 @@ const Status = {
     scrollIfHighlighted (highlightId) {
       const id = highlightId
       if (this.status.id === id) {
-        let rect = this.$el.getBoundingClientRect()
+        const rect = this.$el.getBoundingClientRect()
         if (rect.top < 100) {
           // Post is above screen, match its top to screen top
           window.scrollBy(0, rect.top - 100)
@@ -463,7 +463,7 @@ const Status = {
     }
   },
   watch: {
-    'highlight': function (id) {
+    highlight: function (id) {
       this.scrollIfHighlighted(id)
     },
     'status.repeat_num': function (num) {
@@ -478,7 +478,7 @@ const Status = {
         this.$store.dispatch('fetchFavs', this.status.id)
       }
     },
-    'isSuspendable': function (val) {
+    isSuspendable: function (val) {
       this.suspendable = val
     }
   }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index bbb41d41..967a966c 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -122,10 +122,13 @@
           v-if="!noHeading"
           class="left-side"
         >
-          <a :href="$router.resolve(userProfileLink).href" @click.prevent>
+          <a
+            :href="$router.resolve(userProfileLink).href"
+            @click.prevent
+          >
             <UserPopover
-              :userId="status.user.id"
-              :overlayCenters="true"
+              :user-id="status.user.id"
+              :overlay-centers="true"
             >
               <UserAvatar
                 class="post-avatar"
@@ -488,6 +491,6 @@
   </div>
 </template>
 
-<script src="./status.js" ></script>
+<script src="./status.js"></script>
 
 <style src="./status.scss" lang="scss"></style>
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 976fe98c..fb356360 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -96,5 +96,5 @@
     <slot v-if="!hideSubjectStatus" />
   </div>
 </template>
-<script src="./status_body.js" ></script>
+<script src="./status_body.js"></script>
 <style lang="scss" src="./status_body.scss" />
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 9e7d7956..e2120f7a 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -56,7 +56,7 @@
   </div>
 </template>
 
-<script src="./status_content.js" ></script>
+<script src="./status_content.js"></script>
 <style lang="scss">
 .StatusContent {
   flex: 1;
diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue
index 9cabb751..f4ab357b 100644
--- a/src/components/status_popover/status_popover.vue
+++ b/src/components/status_popover/status_popover.vue
@@ -1,16 +1,16 @@
 <template>
   <Popover
+    ref="popover"
     trigger="hover"
     :stay-on-click="true"
     popover-class="popover-default status-popover"
     :bound-to="{ x: 'container' }"
     @show="enter"
-    ref="popover"
   >
-    <template v-slot:trigger>
+    <template #trigger>
       <slot />
     </template>
-    <template v-slot:content>
+    <template #content>
       <Status
         v-if="status"
         :is-preview="true"
@@ -37,7 +37,7 @@
   </Popover>
 </template>
 
-<script src="./status_popover.js" ></script>
+<script src="./status_popover.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
index 3a2d3914..b06384e5 100644
--- a/src/components/sticker_picker/sticker_picker.js
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -31,8 +31,8 @@ const StickerPicker = {
       fetch(sticker)
         .then((res) => {
           res.blob().then((blob) => {
-            var file = new File([blob], name, { mimetype: 'image/png' })
-            var formData = new FormData()
+            const file = new File([blob], name, { mimetype: 'image/png' })
+            const formData = new FormData()
             formData.append('file', file)
             statusPosterService.uploadMedia({ store, formData })
               .then((fileData) => {
diff --git a/src/components/terms_of_service_panel/terms_of_service_panel.vue b/src/components/terms_of_service_panel/terms_of_service_panel.vue
index 63dc58b8..1df41d70 100644
--- a/src/components/terms_of_service_panel/terms_of_service_panel.vue
+++ b/src/components/terms_of_service_panel/terms_of_service_panel.vue
@@ -13,7 +13,7 @@
   </div>
 </template>
 
-<script src="./terms_of_service_panel.js" ></script>
+<script src="./terms_of_service_panel.js"></script>
 
 <style lang="scss">
 .tos-content {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index f65881b6..266c1d9a 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -46,7 +46,10 @@
       </div>
     </div>
     <div :class="classes.footer">
-      <teleport :to="footerSlipgate" :disabled="!embedded || !footerSlipgate">
+      <teleport
+        :to="footerSlipgate"
+        :disabled="!embedded || !footerSlipgate"
+      >
         <div
           v-if="count===0"
           class="new-status-notification text-center faint"
diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue
index 98fab926..297bc72a 100644
--- a/src/components/timeline/timeline_quick_settings.vue
+++ b/src/components/timeline/timeline_quick_settings.vue
@@ -4,7 +4,7 @@
     class="TimelineQuickSettings"
     :bound-to="{ x: 'container' }"
   >
-    <template v-slot:content>
+    <template #content>
       <div class="dropdown-menu">
         <div v-if="loggedIn">
           <button
@@ -80,7 +80,7 @@
         </button>
       </div>
     </template>
-    <template v-slot:trigger>
+    <template #trigger>
       <button class="button-unstyled">
         <FAIcon icon="filter" />
       </button>
diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js
index bab51e75..a11e7b7e 100644
--- a/src/components/timeline_menu/timeline_menu.js
+++ b/src/components/timeline_menu/timeline_menu.js
@@ -11,9 +11,9 @@ library.add(faChevronDown)
 // because nav panel benefits from the same information.
 export const timelineNames = () => {
   return {
-    'friends': 'nav.home_timeline',
-    'bookmarks': 'nav.bookmarks',
-    'dms': 'nav.dms',
+    friends: 'nav.home_timeline',
+    bookmarks: 'nav.bookmarks',
+    dms: 'nav.dms',
     'public-timeline': 'nav.public_tl',
     'public-external-timeline': 'nav.twkn'
   }
diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue
index 1e054f5a..c24b9d72 100644
--- a/src/components/timeline_menu/timeline_menu.vue
+++ b/src/components/timeline_menu/timeline_menu.vue
@@ -9,10 +9,10 @@
     @show="openMenu"
     @close="() => isOpen = false"
   >
-    <template v-slot:content>
+    <template #content>
       <TimelineMenuContent />
     </template>
-    <template v-slot:trigger>
+    <template #trigger>
       <span class="button-unstyled title timeline-menu-title">
         <span class="timeline-title">{{ timelineName() }}</span>
         <span>
@@ -30,7 +30,7 @@
   </Popover>
 </template>
 
-<script src="./timeline_menu.js" ></script>
+<script src="./timeline_menu.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue
index bed1b679..59e9e43c 100644
--- a/src/components/timeline_menu/timeline_menu_content.vue
+++ b/src/components/timeline_menu/timeline_menu_content.vue
@@ -63,4 +63,4 @@
   </ul>
 </template>
 
-<script src="./timeline_menu_content.js" ></script>
+<script src="./timeline_menu_content.js"></script>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index b0fa11b3..4c81e2c7 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -67,7 +67,7 @@ export default {
     style () {
       return {
         backgroundImage: [
-          `linear-gradient(to bottom, var(--profileTint), var(--profileTint))`,
+          'linear-gradient(to bottom, var(--profileTint), var(--profileTint))',
           `url(${this.user.cover_photo})`
         ].join(', ')
       }
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index bc23e68e..ace89c51 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -29,10 +29,10 @@
           </a>
           <UserAvatar
             v-else-if="typeof avatarAction === 'function'"
-            @click="avatarAction"
             class="user-info-avatar"
             :better-shadow="betterShadow"
             :user="user"
+            @click="avatarAction"
           />
           <router-link
             v-else
diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue
index bdc3aa92..a3ce54c3 100644
--- a/src/components/user_list_popover/user_list_popover.vue
+++ b/src/components/user_list_popover/user_list_popover.vue
@@ -4,10 +4,10 @@
     placement="top"
     :offset="{ y: 5 }"
   >
-    <template v-slot:trigger>
+    <template #trigger>
       <slot />
     </template>
-    <template v-slot:content>
+    <template #content>
       <div class="user-list-popover">
         <template v-if="users.length">
           <div
@@ -45,7 +45,7 @@
   </Popover>
 </template>
 
-<script src="./user_list_popover.js" ></script>
+<script src="./user_list_popover.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue
index 72bb7f77..4e999672 100644
--- a/src/components/user_popover/user_popover.vue
+++ b/src/components/user_popover/user_popover.vue
@@ -1,27 +1,27 @@
 <template>
-<Popover
-  trigger="click"
-  popover-class="popover-default user-popover"
-  :overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
-  :overlay-centers="overlayCenters && userPopoverOverlay"
-  :disabled="disabled"
->
-  <template v-slot:trigger>
-    <slot />
-  </template>
-  <template v-slot:content={close}>
-    <UserCard
-      class="user-popover"
-      :user-id="userId"
-      :hide-bio="true"
-      :avatar-action="userPopoverZoom ? 'zoom' : close"
-      :on-close="close"
-    />
-  </template>
-</Popover>
+  <Popover
+    trigger="click"
+    popover-class="popover-default user-popover"
+    :overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
+    :overlay-centers="overlayCenters && userPopoverOverlay"
+    :disabled="disabled"
+  >
+    <template #trigger>
+      <slot />
+    </template>
+    <template #content="{close}">
+      <UserCard
+        class="user-popover"
+        :user-id="userId"
+        :hide-bio="true"
+        :avatar-action="userPopoverZoom ? 'zoom' : close"
+        :on-close="close"
+      />
+    </template>
+  </Popover>
 </template>
 
-<script src="./user_popover.js" ></script>
+<script src="./user_popover.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index dbf28981..d0da2b5b 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -56,7 +56,7 @@
           :user-id="userId"
           :pinned-status-ids="user.pinnedStatusIds"
           :in-profile="true"
-          :footerSlipgate="footerRef"
+          :footer-slipgate="footerRef"
         />
         <div
           v-if="followsTabVisible"
@@ -65,7 +65,7 @@
           :disabled="!user.friends_count"
         >
           <FriendList :user-id="userId">
-            <template v-slot:item="{item}">
+            <template #item="{item}">
               <FollowCard :user="item" />
             </template>
           </FriendList>
@@ -77,7 +77,7 @@
           :disabled="!user.followers_count"
         >
           <FollowerList :user-id="userId">
-            <template v-slot:item="{item}">
+            <template #item="{item}">
               <FollowCard
                 :user="item"
                 :no-follows-you="isUs"
@@ -95,7 +95,7 @@
           :timeline="media"
           :user-id="userId"
           :in-profile="true"
-          :footerSlipgate="footerRef"
+          :footer-slipgate="footerRef"
         />
         <Timeline
           v-if="isUs"
@@ -107,10 +107,13 @@
           timeline-name="favorites"
           :timeline="favorites"
           :in-profile="true"
-          :footerSlipgate="footerRef"
+          :footer-slipgate="footerRef"
         />
       </tab-switcher>
-      <div class="panel-footer" :ref="setFooterRef"></div>
+      <div
+        :ref="setFooterRef"
+        class="panel-footer"
+      />
     </div>
     <div
       v-else
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 030ce2c4..429a66e2 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -45,7 +45,7 @@
         </div>
         <div class="user-reporting-panel-right">
           <List :items="statuses">
-            <template v-slot:item="{item}">
+            <template #item="{item}">
               <div class="status-fadein user-reporting-panel-sitem">
                 <Status
                   :in-conversation="false"
diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js
index ecd97dd7..53f05272 100644
--- a/src/components/who_to_follow/who_to_follow.js
+++ b/src/components/who_to_follow/who_to_follow.js
@@ -28,7 +28,7 @@ const WhoToFollow = {
     getWhoToFollow () {
       const credentials = this.$store.state.users.currentUser.credentials
       if (credentials) {
-        apiService.suggestions({ credentials: credentials })
+        apiService.suggestions({ credentials })
           .then((reply) => {
             this.showWhoToFollow(reply)
           })
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js
index 818e8bd5..f19ba948 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.js
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.js
@@ -6,9 +6,9 @@ function showWhoToFollow (panel, reply) {
   const shuffled = shuffle(reply)
 
   panel.usersToFollow.forEach((toFollow, index) => {
-    let user = shuffled[index]
-    let img = user.avatar || this.$store.state.instance.defaultAvatar
-    let name = user.acct
+    const user = shuffled[index]
+    const img = user.avatar || this.$store.state.instance.defaultAvatar
+    const name = user.acct
 
     toFollow.img = img
     toFollow.name = name
@@ -24,12 +24,12 @@ function showWhoToFollow (panel, reply) {
 }
 
 function getWhoToFollow (panel) {
-  var credentials = panel.$store.state.users.currentUser.credentials
+  const credentials = panel.$store.state.users.currentUser.credentials
   if (credentials) {
     panel.usersToFollow.forEach(toFollow => {
       toFollow.name = 'Loading...'
     })
-    apiService.suggestions({ credentials: credentials })
+    apiService.suggestions({ credentials })
       .then((reply) => {
         showWhoToFollow(panel, reply)
       })
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.vue b/src/components/who_to_follow_panel/who_to_follow_panel.vue
index 518acd97..c1ba6fb1 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.vue
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.vue
@@ -27,7 +27,7 @@
   </div>
 </template>
 
-<script src="./who_to_follow_panel.js" ></script>
+<script src="./who_to_follow_panel.js"></script>
 
 <style lang="scss">
   .who-to-follow * {
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 18ed79b7..eae75c80 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -46,7 +46,7 @@ const messages = {
   },
   setLanguage: async (i18n, language) => {
     if (loaders[language]) {
-      let messages = await loaders[language]()
+      const messages = await loaders[language]()
       i18n.setLocaleMessage(language, messages.default)
     }
     i18n.locale = language
diff --git a/src/lib/notification-i18n-loader.js b/src/lib/notification-i18n-loader.js
index 71f9156a..d7a4430d 100644
--- a/src/lib/notification-i18n-loader.js
+++ b/src/lib/notification-i18n-loader.js
@@ -3,8 +3,8 @@
 // meant to be used to load the partial i18n we need for
 // the service worker.
 module.exports = function (source) {
-  var object = JSON.parse(source)
-  var smol = {
+  const object = JSON.parse(source)
+  const smol = {
     notifications: object.notifications || {}
   }
 
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index 24b835da..c73a38ec 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -5,10 +5,12 @@ import { each, get, set, cloneDeep } from 'lodash'
 let loaded = false
 
 const defaultReducer = (state, paths) => (
-  paths.length === 0 ? state : paths.reduce((substate, path) => {
-    set(substate, path, get(state, path))
-    return substate
-  }, {})
+  paths.length === 0
+    ? state
+    : paths.reduce((substate, path) => {
+      set(substate, path, get(state, path))
+      return substate
+    }, {})
 )
 
 const saveImmedeatelyActions = [
@@ -30,7 +32,7 @@ export default function createPersistedState ({
   key = 'vuex-lz',
   paths = [],
   getState = (key, storage) => {
-    let value = storage.getItem(key)
+    const value = storage.getItem(key)
     return value
   },
   setState = (key, state, storage) => {
diff --git a/src/modules/api.js b/src/modules/api.js
index 54f94356..28f2076e 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -233,7 +233,7 @@ const api = {
 
     // Follow requests
     startFetchingFollowRequests (store) {
-      if (store.state.fetchers['followRequests']) return
+      if (store.state.fetchers.followRequests) return
       const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
 
       store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
@@ -244,7 +244,7 @@ const api = {
       store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
     },
     removeFollowRequest (store, request) {
-      let requests = store.state.followRequests.filter((it) => it !== request)
+      const requests = store.state.followRequests.filter((it) => it !== request)
       store.commit('setFollowRequests', requests)
     },
 
diff --git a/src/modules/config.js b/src/modules/config.js
index f7f142ad..eaf67a91 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -147,7 +147,7 @@ const config = {
       const knownKeys = new Set(Object.keys(defaultState))
       const presentKeys = new Set(Object.keys(data))
       const intersection = new Set()
-      for (let elem of presentKeys) {
+      for (const elem of presentKeys) {
         if (knownKeys.has(elem)) {
           intersection.add(elem)
         }
diff --git a/src/modules/errors.js b/src/modules/errors.js
index ca89dc0f..d2e24100 100644
--- a/src/modules/errors.js
+++ b/src/modules/errors.js
@@ -2,8 +2,8 @@ import { capitalize } from 'lodash'
 
 export function humanizeErrors (errors) {
   return Object.entries(errors).reduce((errs, [k, val]) => {
-    let message = val.reduce((acc, message) => {
-      let key = capitalize(k.replace(/_/g, ' '))
+    const message = val.reduce((acc, message) => {
+      const key = capitalize(k.replace(/_/g, ' '))
       return acc + [key, message].join(' ') + '. '
     }, '')
     return [...errs, message]
diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js
index 4b73af26..476263bc 100644
--- a/src/modules/serverSideConfig.js
+++ b/src/modules/serverSideConfig.js
@@ -39,53 +39,53 @@ const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
  * If no api is specified, defaultApi is used (see above)
  */
 export const settingsMap = {
-  'defaultScope': 'source.privacy',
-  'defaultNSFW': 'source.sensitive', // BROKEN: pleroma/pleroma#2837
-  'stripRichContent': {
+  defaultScope: 'source.privacy',
+  defaultNSFW: 'source.sensitive', // BROKEN: pleroma/pleroma#2837
+  stripRichContent: {
     get: 'source.pleroma.no_rich_text',
     set: 'no_rich_text'
   },
   // Privacy
-  'locked': 'locked',
-  'acceptChatMessages': {
+  locked: 'locked',
+  acceptChatMessages: {
     get: 'pleroma.accepts_chat_messages',
     set: 'accepts_chat_messages'
   },
-  'allowFollowingMove': {
+  allowFollowingMove: {
     get: 'pleroma.allow_following_move',
     set: 'allow_following_move'
   },
-  'discoverable': {
+  discoverable: {
     get: 'source.pleroma.discoverable',
     set: 'discoverable'
   },
-  'hideFavorites': {
+  hideFavorites: {
     get: 'pleroma.hide_favorites',
     set: 'hide_favorites'
   },
-  'hideFollowers': {
+  hideFollowers: {
     get: 'pleroma.hide_followers',
     set: 'hide_followers'
   },
-  'hideFollows': {
+  hideFollows: {
     get: 'pleroma.hide_follows',
     set: 'hide_follows'
   },
-  'hideFollowersCount': {
+  hideFollowersCount: {
     get: 'pleroma.hide_followers_count',
     set: 'hide_followers_count'
   },
-  'hideFollowsCount': {
+  hideFollowsCount: {
     get: 'pleroma.hide_follows_count',
     set: 'hide_follows_count'
   },
   // NotificationSettingsAPIs
-  'webPushHideContents': {
+  webPushHideContents: {
     get: 'pleroma.notification_settings.hide_notification_contents',
     set: 'hide_notification_contents',
     api: notificationsApi
   },
-  'blockNotificationsFromStrangers': {
+  blockNotificationsFromStrangers: {
     get: 'pleroma.notification_settings.block_from_strangers',
     set: 'block_from_strangers',
     api: notificationsApi
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index a13930e9..67026832 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -245,10 +245,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
   }
 
   const processors = {
-    'status': (status) => {
+    status: (status) => {
       addStatus(status, showImmediately)
     },
-    'retweet': (status) => {
+    retweet: (status) => {
       // RetweetedStatuses are never shown immediately
       const retweetedStatus = addStatus(status.retweeted_status, false, false)
 
@@ -270,7 +270,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 
       retweet.retweeted_status = retweetedStatus
     },
-    'favorite': (favorite) => {
+    favorite: (favorite) => {
       // Only update if this is a new favorite.
       // Ignore our own favorites because we get info about likes as response to like request
       if (!state.favorites.has(favorite.id)) {
@@ -278,7 +278,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
         favoriteStatus(favorite)
       }
     },
-    'deletion': (deletion) => {
+    deletion: (deletion) => {
       const uri = deletion.uri
       const status = find(allStatuses, { uri })
       if (!status) {
@@ -292,10 +292,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
         remove(timelineObject.visibleStatuses, { uri })
       }
     },
-    'follow': (follow) => {
+    follow: (follow) => {
       // NOOP, it is known status but we don't do anything about it for now
     },
-    'default': (unknown) => {
+    default: (unknown) => {
       console.log('unknown status type')
       console.log(unknown)
     }
@@ -303,7 +303,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 
   each(statuses, (status) => {
     const type = status.type
-    const processor = processors[type] || processors['default']
+    const processor = processors[type] || processors.default
     processor(status)
   })
 
@@ -522,7 +522,7 @@ export const mutations = {
   },
   addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
     const status = state.allStatusesObject[id]
-    status['emoji_reactions'] = emojiReactions
+    status.emoji_reactions = emojiReactions
   },
   addOwnReaction (state, { id, emoji, currentUser }) {
     const status = state.allStatusesObject[id]
@@ -543,7 +543,7 @@ export const mutations = {
     if (reactionIndex >= 0) {
       status.emoji_reactions[reactionIndex] = newReaction
     } else {
-      status['emoji_reactions'] = [...status.emoji_reactions, newReaction]
+      status.emoji_reactions = [...status.emoji_reactions, newReaction]
     }
   },
   removeOwnReaction (state, { id, emoji, currentUser }) {
@@ -564,7 +564,7 @@ export const mutations = {
     if (newReaction.count > 0) {
       status.emoji_reactions[reactionIndex] = newReaction
     } else {
-      status['emoji_reactions'] = status.emoji_reactions.filter(r => r.name !== emoji)
+      status.emoji_reactions = status.emoji_reactions.filter(r => r.name !== emoji)
     }
   },
   updateStatusWithPoll (state, { id, poll }) {
diff --git a/src/modules/users.js b/src/modules/users.js
index f951483f..13d4e318 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -103,23 +103,23 @@ export const mutations = {
     const user = state.usersObject[id]
     const tags = user.tags || []
     const newTags = tags.concat([tag])
-    user['tags'] = newTags
+    user.tags = newTags
   },
   untagUser (state, { user: { id }, tag }) {
     const user = state.usersObject[id]
     const tags = user.tags || []
     const newTags = tags.filter(t => t !== tag)
-    user['tags'] = newTags
+    user.tags = newTags
   },
   updateRight (state, { user: { id }, right, value }) {
     const user = state.usersObject[id]
-    let newRights = user.rights
+    const newRights = user.rights
     newRights[right] = value
-    user['rights'] = newRights
+    user.rights = newRights
   },
   updateActivationStatus (state, { user: { id }, deactivated }) {
     const user = state.usersObject[id]
-    user['deactivated'] = deactivated
+    user.deactivated = deactivated
   },
   setCurrentUser (state, user) {
     state.lastLoginName = user.screen_name
@@ -148,13 +148,13 @@ export const mutations = {
   clearFriends (state, userId) {
     const user = state.usersObject[userId]
     if (user) {
-      user['friendIds'] = []
+      user.friendIds = []
     }
   },
   clearFollowers (state, userId) {
     const user = state.usersObject[userId]
     if (user) {
-      user['followerIds'] = []
+      user.followerIds = []
     }
   },
   addNewUsers (state, users) {
@@ -222,7 +222,7 @@ export const mutations = {
   },
   setColor (state, { user: { id }, highlighted }) {
     const user = state.usersObject[id]
-    user['highlight'] = highlighted
+    user.highlight = highlighted
   },
   signUpPending (state) {
     state.signUpPending = true
@@ -393,7 +393,7 @@ const users = {
     toggleActivationStatus ({ rootState, commit }, { user }) {
       const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
       api({ user })
-        .then((user) => { let deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
+        .then((user) => { const deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
     },
     registerPushNotifications (store) {
       const token = store.state.currentUser.credentials
@@ -457,17 +457,17 @@ const users = {
     async signUp (store, userInfo) {
       store.commit('signUpPending')
 
-      let rootState = store.rootState
+      const rootState = store.rootState
 
       try {
-        let data = await rootState.api.backendInteractor.register(
+        const data = await rootState.api.backendInteractor.register(
           { params: { ...userInfo } }
         )
         store.commit('signUpSuccess')
         store.commit('setToken', data.access_token)
         store.dispatch('loginUser', data.access_token)
       } catch (e) {
-        let errors = e.message
+        const errors = e.message
         store.commit('signUpFailure', errors)
         throw e
       }
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index e413baee..da3bccc5 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -76,7 +76,7 @@ const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
 const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
 const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
 const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
-const MASTODON_SEARCH_2 = `/api/v2/search`
+const MASTODON_SEARCH_2 = '/api/v2/search'
 const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
 const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_STREAMING = '/api/v1/streaming'
@@ -84,7 +84,7 @@ 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_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`
@@ -93,7 +93,7 @@ const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
 
 const oldfetch = window.fetch
 
-let fetch = (url, options) => {
+const fetch = (url, options) => {
   options = options || {}
   const baseUrl = ''
   const fullUrl = baseUrl + url
@@ -105,7 +105,7 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
   const options = {
     method,
     headers: {
-      'Accept': 'application/json',
+      Accept: 'application/json',
       'Content-Type': 'application/json',
       ...headers
     }
@@ -229,16 +229,16 @@ const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
 
 const authHeaders = (accessToken) => {
   if (accessToken) {
-    return { 'Authorization': `Bearer ${accessToken}` }
+    return { Authorization: `Bearer ${accessToken}` }
   } else {
     return { }
   }
 }
 
 const followUser = ({ id, credentials, ...options }) => {
-  let url = MASTODON_FOLLOW_URL(id)
+  const url = MASTODON_FOLLOW_URL(id)
   const form = {}
-  if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
+  if (options.reblogs !== undefined) { form.reblogs = options.reblogs }
   return fetch(url, {
     body: JSON.stringify(form),
     headers: {
@@ -250,7 +250,7 @@ const followUser = ({ id, credentials, ...options }) => {
 }
 
 const unfollowUser = ({ id, credentials }) => {
-  let url = MASTODON_UNFOLLOW_URL(id)
+  const url = MASTODON_UNFOLLOW_URL(id)
   return fetch(url, {
     headers: authHeaders(credentials),
     method: 'POST'
@@ -292,7 +292,7 @@ const unblockUser = ({ id, credentials }) => {
 }
 
 const approveUser = ({ id, credentials }) => {
-  let url = MASTODON_APPROVE_USER_URL(id)
+  const url = MASTODON_APPROVE_USER_URL(id)
   return fetch(url, {
     headers: authHeaders(credentials),
     method: 'POST'
@@ -300,7 +300,7 @@ const approveUser = ({ id, credentials }) => {
 }
 
 const denyUser = ({ id, credentials }) => {
-  let url = MASTODON_DENY_USER_URL(id)
+  const url = MASTODON_DENY_USER_URL(id)
   return fetch(url, {
     headers: authHeaders(credentials),
     method: 'POST'
@@ -308,13 +308,13 @@ const denyUser = ({ id, credentials }) => {
 }
 
 const fetchUser = ({ id, credentials }) => {
-  let url = `${MASTODON_USER_URL}/${id}`
+  const url = `${MASTODON_USER_URL}/${id}`
   return promisedRequest({ url, credentials })
     .then((data) => parseUser(data))
 }
 
 const fetchUserRelationship = ({ id, credentials }) => {
-  let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
+  const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
   return fetch(url, { headers: authHeaders(credentials) })
     .then((response) => {
       return new Promise((resolve, reject) => response.json()
@@ -333,7 +333,7 @@ const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => {
     maxId && `max_id=${maxId}`,
     sinceId && `since_id=${sinceId}`,
     limit && `limit=${limit}`,
-    `with_relationships=true`
+    'with_relationships=true'
   ].filter(_ => _).join('&')
 
   url = url + (args ? '?' + args : '')
@@ -368,7 +368,7 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
     maxId && `max_id=${maxId}`,
     sinceId && `since_id=${sinceId}`,
     limit && `limit=${limit}`,
-    `with_relationships=true`
+    'with_relationships=true'
   ].filter(_ => _).join('&')
 
   url += args ? '?' + args : ''
@@ -385,7 +385,7 @@ const fetchFollowRequests = ({ credentials }) => {
 }
 
 const fetchConversation = ({ id, credentials }) => {
-  let urlContext = MASTODON_STATUS_CONTEXT_URL(id)
+  const urlContext = MASTODON_STATUS_CONTEXT_URL(id)
   return fetch(urlContext, { headers: authHeaders(credentials) })
     .then((data) => {
       if (data.ok) {
@@ -401,7 +401,7 @@ const fetchConversation = ({ id, credentials }) => {
 }
 
 const fetchStatus = ({ id, credentials }) => {
-  let url = MASTODON_STATUS_URL(id)
+  const url = MASTODON_STATUS_URL(id)
   return fetch(url, { headers: authHeaders(credentials) })
     .then((data) => {
       if (data.ok) {
@@ -425,7 +425,7 @@ const tagUser = ({ tag, credentials, user }) => {
 
   return fetch(TAG_USER_URL, {
     method: 'PUT',
-    headers: headers,
+    headers,
     body: JSON.stringify(form)
   })
 }
@@ -442,7 +442,7 @@ const untagUser = ({ tag, credentials, user }) => {
 
   return fetch(TAG_USER_URL, {
     method: 'DELETE',
-    headers: headers,
+    headers,
     body: JSON.stringify(body)
   })
 }
@@ -495,7 +495,7 @@ const deleteUser = ({ credentials, user }) => {
 
   return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
     method: 'DELETE',
-    headers: headers
+    headers
   })
 }
 
@@ -514,7 +514,7 @@ const fetchTimeline = ({
     friends: MASTODON_USER_HOME_TIMELINE_URL,
     dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
     notifications: MASTODON_USER_NOTIFICATIONS_URL,
-    'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
+    publicAndExternal: MASTODON_PUBLIC_TIMELINE,
     user: MASTODON_USER_TIMELINE_URL,
     media: MASTODON_USER_TIMELINE_URL,
     favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
@@ -688,7 +688,7 @@ const postStatus = ({
     form.append('preview', 'true')
   }
 
-  let postHeaders = authHeaders(credentials)
+  const postHeaders = authHeaders(credentials)
   if (idempotencyKey) {
     postHeaders['idempotency-key'] = idempotencyKey
   }
@@ -993,7 +993,7 @@ const vote = ({ pollId, choices, credentials }) => {
     method: 'POST',
     credentials,
     payload: {
-      choices: choices
+      choices
     }
   })
 }
@@ -1053,8 +1053,8 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
     url: MASTODON_REPORT_USER_URL,
     method: 'POST',
     payload: {
-      'account_id': userId,
-      'status_ids': statusIds,
+      account_id: userId,
+      status_ids: statusIds,
       comment,
       forward
     },
@@ -1076,7 +1076,7 @@ const searchUsers = ({ credentials, query }) => {
 
 const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
   let url = MASTODON_SEARCH_2
-  let params = []
+  const params = []
 
   if (q) {
     params.push(['q', encodeURIComponent(q)])
@@ -1100,7 +1100,7 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
 
   params.push(['with_relationships', true])
 
-  let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
+  const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
   url += `?${queryString}`
 
   return fetch(url, { headers: authHeaders(credentials) })
@@ -1254,12 +1254,12 @@ export const handleMastoWS = (wsEvent) => {
 }
 
 export const WSConnectionStatus = Object.freeze({
-  'JOINED': 1,
-  'CLOSED': 2,
-  'ERROR': 3,
-  'DISABLED': 4,
-  'STARTING': 5,
-  'STARTING_INITIAL': 6
+  JOINED: 1,
+  CLOSED: 2,
+  ERROR: 3,
+  DISABLED: 4,
+  STARTING: 5,
+  STARTING_INITIAL: 6
 })
 
 const chats = ({ credentials }) => {
@@ -1297,11 +1297,11 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
 
 const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => {
   const payload = {
-    'content': content
+    content
   }
 
   if (mediaId) {
-    payload['media_id'] = mediaId
+    payload.media_id = mediaId
   }
 
   const headers = {}
@@ -1313,7 +1313,7 @@ const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credenti
   return promisedRequest({
     url: PLEROMA_CHAT_MESSAGES_URL(id),
     method: 'POST',
-    payload: payload,
+    payload,
     credentials,
     headers
   })
@@ -1324,7 +1324,7 @@ const readChat = ({ id, lastReadId, credentials }) => {
     url: PLEROMA_CHAT_READ_URL(id),
     method: 'POST',
     payload: {
-      'last_read_id': lastReadId
+      last_read_id: lastReadId
     },
     credentials
   })
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
index 92ff689d..eb26a0ab 100644
--- a/src/services/chat_service/chat_service.js
+++ b/src/services/chat_service/chat_service.js
@@ -7,7 +7,7 @@ const empty = (chatId) => {
     messages: [],
     newMessageCount: 0,
     lastSeenMessageId: '0',
-    chatId: chatId,
+    chatId,
     minId: undefined,
     maxId: undefined
   }
@@ -101,7 +101,7 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
           storage.messages = storage.messages.filter(msg => msg.id !== message.id)
         }
         Object.assign(fakeMessage, message, { error: false })
-        delete fakeMessage['fakeId']
+        delete fakeMessage.fakeId
         storage.idIndex[fakeMessage.id] = fakeMessage
         delete storage.idIndex[message.fakeId]
 
@@ -178,7 +178,7 @@ const getView = (storage) => {
         id: date.getTime().toString()
       })
 
-      previousMessage['isTail'] = true
+      previousMessage.isTail = true
       currentMessageChainId = undefined
       afterDate = true
     }
@@ -193,15 +193,15 @@ const getView = (storage) => {
 
     // end a message chian
     if ((nextMessage && nextMessage.account_id) !== message.account_id) {
-      object['isTail'] = true
+      object.isTail = true
       currentMessageChainId = undefined
     }
 
     // start a new message chain
     if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) {
       currentMessageChainId = _.uniqueId()
-      object['isHead'] = true
-      object['messageChainId'] = currentMessageChainId
+      object.isHead = true
+      object.messageChainId = currentMessageChainId
     }
 
     result.push(object)
diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js
index de6e0625..a8da1eed 100644
--- a/src/services/chat_utils/chat_utils.js
+++ b/src/services/chat_utils/chat_utils.js
@@ -25,7 +25,7 @@ export const buildFakeMessage = ({ content, chatId, attachments, userId, idempot
     chat_id: chatId,
     created_at: new Date(),
     id: `${new Date().getTime()}`,
-    attachments: attachments,
+    attachments,
     account_id: userId,
     idempotency_key: idempotencyKey,
     emojis: [],
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
index ec104269..47d6344e 100644
--- a/src/services/color_convert/color_convert.js
+++ b/src/services/color_convert/color_convert.js
@@ -144,11 +144,13 @@ export const invert = (rgb) => {
  */
 export const hex2rgb = (hex) => {
   const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
-  return result ? {
-    r: parseInt(result[1], 16),
-    g: parseInt(result[2], 16),
-    b: parseInt(result[3], 16)
-  } : null
+  return result
+    ? {
+        r: parseInt(result[1], 16),
+        g: parseInt(result[2], 16),
+        b: parseInt(result[3], 16)
+      }
+    : null
 }
 
 /**
diff --git a/src/services/completion/completion.js b/src/services/completion/completion.js
index 8a6eba7e..8fa4f75b 100644
--- a/src/services/completion/completion.js
+++ b/src/services/completion/completion.js
@@ -35,7 +35,7 @@ export const addPositionToWords = (words) => {
 }
 
 export const splitByWhitespaceBoundary = (str) => {
-  let result = []
+  const result = []
   let currentWord = ''
   for (let i = 0; i < str.length; i++) {
     const currentChar = str[i]
diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js
index 677c184c..c93d2176 100644
--- a/src/services/date_utils/date_utils.js
+++ b/src/services/date_utils/date_utils.js
@@ -10,7 +10,7 @@ export const relativeTime = (date, nowThreshold = 1) => {
   if (typeof date === 'string') date = Date.parse(date)
   const round = Date.now() > date ? Math.floor : Math.ceil
   const d = Math.abs(Date.now() - date)
-  let r = { num: round(d / YEAR), key: 'time.unit.years' }
+  const r = { num: round(d / YEAR), key: 'time.unit.years' }
   if (d < nowThreshold * SECOND) {
     r.num = 0
     r.key = 'time.now'
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index f219c161..86472b42 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -371,8 +371,8 @@ export const parseStatus = (data) => {
 
 export const parseNotification = (data) => {
   const mastoDict = {
-    'favourite': 'like',
-    'reblog': 'repeat'
+    favourite: 'like',
+    reblog: 'repeat'
   }
   const masto = !data.hasOwnProperty('ntype')
   const output = {}
diff --git a/src/services/file_size_format/file_size_format.js b/src/services/file_size_format/file_size_format.js
index 7e6cd4d7..d481dfb0 100644
--- a/src/services/file_size_format/file_size_format.js
+++ b/src/services/file_size_format/file_size_format.js
@@ -1,7 +1,7 @@
 const fileSizeFormat = (num) => {
-  var exponent
-  var unit
-  var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
+  let exponent
+  let unit
+  const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
   if (num < 1) {
     return num + ' ' + units[0]
   }
@@ -9,7 +9,7 @@ const fileSizeFormat = (num) => {
   exponent = Math.min(Math.floor(Math.log(num) / Math.log(1024)), units.length - 1)
   num = (num / Math.pow(1024, exponent)).toFixed(2) * 1
   unit = units[exponent]
-  return { num: num, unit: unit }
+  return { num, unit }
 }
 const fileSizeFormatService = {
   fileSizeFormat
diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js
index 5eeaa7cb..9c3d1f19 100644
--- a/src/services/html_converter/html_line_converter.service.js
+++ b/src/services/html_converter/html_line_converter.service.js
@@ -46,7 +46,7 @@ export const convertHtmlToLines = (html = '') => {
   // All block-level elements that aren't empty elements, i.e. not <hr>
   const nonEmptyElements = new Set(visualLineElements)
   // Difference
-  for (let elem of emptyElements) {
+  for (const elem of emptyElements) {
     nonEmptyElements.delete(elem)
   }
 
@@ -56,7 +56,7 @@ export const convertHtmlToLines = (html = '') => {
     ...emptyElements.values()
   ])
 
-  let buffer = [] // Current output buffer
+  const buffer = [] // Current output buffer
   const level = [] // How deep we are in tags and which tags were there
   let textBuffer = '' // Current line content
   let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
diff --git a/src/services/html_converter/utility.service.js b/src/services/html_converter/utility.service.js
index 4d0c36c2..583ccca5 100644
--- a/src/services/html_converter/utility.service.js
+++ b/src/services/html_converter/utility.service.js
@@ -50,7 +50,7 @@ export const processTextForEmoji = (text, emojis, processor) => {
     if (char === ':') {
       const next = text.slice(i + 1)
       let found = false
-      for (let emoji of emojis) {
+      for (const emoji of emojis) {
         if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
           found = emoji
           break
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
index 8cef2522..d3389785 100644
--- a/src/services/locale/locale.service.js
+++ b/src/services/locale/locale.service.js
@@ -3,9 +3,9 @@ import ISO6391 from 'iso-639-1'
 import _ from 'lodash'
 
 const specialLanguageCodes = {
-  'ja_easy': 'ja',
-  'zh_Hant': 'zh-HANT',
-  'zh': 'zh-Hans'
+  ja_easy: 'ja',
+  zh_Hant: 'zh-HANT',
+  zh: 'zh-Hans'
 }
 
 const internalToBrowserLocale = code => specialLanguageCodes[code] || code
@@ -14,16 +14,16 @@ const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_
 
 const getLanguageName = (code) => {
   const specialLanguageNames = {
-    'ja_easy': 'やさしいにほんご',
-    'zh': '简体中文',
-    'zh_Hant': '繁體中文'
+    ja_easy: 'やさしいにほんご',
+    zh: '简体中文',
+    zh_Hant: '繁體中文'
   }
   const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
   const browserLocale = internalToBrowserLocale(code)
   return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
 }
 
-const languages = _.map(languagesObject.languages, (code) => ({ code: code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
+const languages = _.map(languagesObject.languages, (code) => ({ code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
 
 const localeService = {
   internalToBrowserLocale,
diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js
index 43199625..9f3c27b5 100644
--- a/src/services/new_api/password_reset.js
+++ b/src/services/new_api/password_reset.js
@@ -1,6 +1,6 @@
 import { reduce } from 'lodash'
 
-const MASTODON_PASSWORD_RESET_URL = `/auth/password`
+const MASTODON_PASSWORD_RESET_URL = '/auth/password'
 
 const resetPassword = ({ instance, email }) => {
   const params = { email }
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index f83f871e..c0e299e4 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -12,20 +12,20 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
   const timelineData = rootState.statuses.notifications
   const hideMutedPosts = getters.mergedConfig.hideMutedPosts
 
-  args['withMuted'] = !hideMutedPosts
+  args.withMuted = !hideMutedPosts
 
-  args['timeline'] = 'notifications'
+  args.timeline = 'notifications'
   if (older) {
     if (timelineData.minId !== Number.POSITIVE_INFINITY) {
-      args['until'] = timelineData.minId
+      args.until = timelineData.minId
     }
     return fetchNotifications({ store, args, older })
   } else {
     // fetch new notifications
     if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) {
-      args['since'] = timelineData.maxId
+      args.since = timelineData.maxId
     } else if (since !== null) {
-      args['since'] = since
+      args.since = since
     }
     const result = fetchNotifications({ store, args, older })
 
@@ -38,7 +38,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
     const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
     const numUnseenNotifs = notifications.length - readNotifsIds.length
     if (numUnseenNotifs > 0 && readNotifsIds.length > 0) {
-      args['since'] = Math.max(...readNotifsIds)
+      args.since = Math.max(...readNotifsIds)
       fetchNotifications({ store, args, older })
     }
 
diff --git a/src/services/push/push.js b/src/services/push/push.js
index 5836fc26..40d7b0fd 100644
--- a/src/services/push/push.js
+++ b/src/services/push/push.js
@@ -43,7 +43,7 @@ function deleteSubscriptionFromBackEnd (token) {
     method: 'DELETE',
     headers: {
       'Content-Type': 'application/json',
-      'Authorization': `Bearer ${token}`
+      Authorization: `Bearer ${token}`
     }
   }).then((response) => {
     if (!response.ok) throw new Error('Bad status code from server.')
@@ -56,7 +56,7 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
-      'Authorization': `Bearer ${token}`
+      Authorization: `Bearer ${token}`
     },
     body: JSON.stringify({
       subscription,
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index b619f810..da48f02c 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -39,7 +39,7 @@ import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
 export const CURRENT_VERSION = 3
 
 export const getLayersArray = (layer, data = LAYERS) => {
-  let array = [layer]
+  const array = [layer]
   let parent = data[layer]
   while (parent) {
     array.unshift(parent)
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 46bba41a..7ba138e0 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -34,20 +34,20 @@ const fetchAndUpdate = ({
   const loggedIn = !!rootState.users.currentUser
 
   if (older) {
-    args['until'] = until || timelineData.minId
+    args.until = until || timelineData.minId
   } else {
     if (since === undefined) {
-      args['since'] = timelineData.maxId
+      args.since = timelineData.maxId
     } else if (since !== null) {
-      args['since'] = since
+      args.since = since
     }
   }
 
-  args['userId'] = userId
-  args['tag'] = tag
-  args['withMuted'] = !hideMutedPosts
+  args.userId = userId
+  args.tag = tag
+  args.withMuted = !hideMutedPosts
   if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) {
-    args['replyVisibility'] = replyVisibility
+    args.replyVisibility = replyVisibility
   }
 
   const numStatusesBeforeFetch = timelineData.statuses.length
@@ -60,7 +60,7 @@ const fetchAndUpdate = ({
 
       const { data: statuses, pagination } = response
       if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
-        store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
+        store.dispatch('queueFlush', { timeline, id: timelineData.maxId })
       }
       update({ store, statuses, timeline, showImmediately, userId, pagination })
       return { statuses, pagination }
diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js
index 3b07592e..b5f58040 100644
--- a/src/services/user_highlighter/user_highlighter.js
+++ b/src/services/user_highlighter/user_highlighter.js
@@ -36,7 +36,7 @@ const highlightStyle = (prefs) => {
         'linear-gradient(to right,',
         `${solidColor} ,`,
         `${solidColor} 2px,`,
-        `transparent 6px`
+        'transparent 6px'
       ].join(' '),
       backgroundPosition: '0 0',
       ...customProps
diff --git a/src/sw.js b/src/sw.js
index 9118dd71..70fed44b 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -57,8 +57,8 @@ self.addEventListener('notificationclick', (event) => {
   event.notification.close()
 
   event.waitUntil(getWindowClients().then((list) => {
-    for (var i = 0; i < list.length; i++) {
-      var client = list[i]
+    for (let i = 0; i < list.length; i++) {
+      const client = list[i]
       if (client.url === '/' && 'focus' in client) { return client.focus() }
     }
 
diff --git a/test/e2e/custom-assertions/elementCount.js b/test/e2e/custom-assertions/elementCount.js
index c0d5fe00..5d5a57b9 100644
--- a/test/e2e/custom-assertions/elementCount.js
+++ b/test/e2e/custom-assertions/elementCount.js
@@ -16,7 +16,7 @@ exports.assertion = function (selector, count) {
     return res.value
   }
   this.command = function (cb) {
-    var self = this
+    const self = this
     return this.api.execute(function (selector) {
       return document.querySelectorAll(selector).length
     }, [selector], function (res) {
diff --git a/test/e2e/nightwatch.conf.js b/test/e2e/nightwatch.conf.js
index 07d974df..4041c698 100644
--- a/test/e2e/nightwatch.conf.js
+++ b/test/e2e/nightwatch.conf.js
@@ -1,45 +1,45 @@
 require('@babel/register')
-var config = require('../../config')
+const config = require('../../config')
 
 // http://nightwatchjs.org/guide#settings-file
 module.exports = {
-  'src_folders': ['test/e2e/specs'],
-  'output_folder': 'test/e2e/reports',
-  'custom_assertions_path': ['test/e2e/custom-assertions'],
+  src_folders: ['test/e2e/specs'],
+  output_folder: 'test/e2e/reports',
+  custom_assertions_path: ['test/e2e/custom-assertions'],
 
-  'selenium': {
-    'start_process': true,
-    'server_path': 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar',
-    'host': '127.0.0.1',
-    'port': 4444,
-    'cli_args': {
+  selenium: {
+    start_process: true,
+    server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar',
+    host: '127.0.0.1',
+    port: 4444,
+    cli_args: {
       'webdriver.chrome.driver': require('chromedriver').path
     }
   },
 
-  'test_settings': {
-    'default': {
-      'selenium_port': 4444,
-      'selenium_host': 'localhost',
-      'silent': true,
-      'globals': {
-        'devServerURL': 'http://localhost:' + (process.env.PORT || config.dev.port)
+  test_settings: {
+    default: {
+      selenium_port: 4444,
+      selenium_host: 'localhost',
+      silent: true,
+      globals: {
+        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
       }
     },
 
-    'chrome': {
-      'desiredCapabilities': {
-        'browserName': 'chrome',
-        'javascriptEnabled': true,
-        'acceptSslCerts': true
+    chrome: {
+      desiredCapabilities: {
+        browserName: 'chrome',
+        javascriptEnabled: true,
+        acceptSslCerts: true
       }
     },
 
-    'firefox': {
-      'desiredCapabilities': {
-        'browserName': 'firefox',
-        'javascriptEnabled': true,
-        'acceptSslCerts': true
+    firefox: {
+      desiredCapabilities: {
+        browserName: 'firefox',
+        javascriptEnabled: true,
+        acceptSslCerts: true
       }
     }
   }
diff --git a/test/e2e/runner.js b/test/e2e/runner.js
index 2a5c6ef9..57abc2e1 100644
--- a/test/e2e/runner.js
+++ b/test/e2e/runner.js
@@ -1,6 +1,6 @@
 // 1. start the dev server using production config
 process.env.NODE_ENV = 'testing'
-var server = require('../../build/dev-server.js')
+const server = require('../../build/dev-server.js')
 
 // 2. run the nightwatch test suite against it
 // to run in additional browsers:
@@ -9,7 +9,7 @@ var server = require('../../build/dev-server.js')
 // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
 // For more information on Nightwatch's config file, see
 // http://nightwatchjs.org/guide#settings-file
-var opts = process.argv.slice(2)
+let opts = process.argv.slice(2)
 if (opts.indexOf('--config') === -1) {
   opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
 }
@@ -17,8 +17,8 @@ if (opts.indexOf('--env') === -1) {
   opts = opts.concat(['--env', 'chrome'])
 }
 
-var spawn = require('cross-spawn')
-var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
+const spawn = require('cross-spawn')
+const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
 
 runner.on('exit', function (code) {
   server.close()
diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js
index 45d74f14..8959e009 100644
--- a/test/unit/karma.conf.js
+++ b/test/unit/karma.conf.js
@@ -4,14 +4,14 @@
 //   https://github.com/webpack/karma-webpack
 
 // var path = require('path')
-var merge = require('webpack-merge')
-var HtmlWebpackPlugin = require('html-webpack-plugin')
-var baseConfig = require('../../build/webpack.base.conf')
-var utils = require('../../build/utils')
-var webpack = require('webpack')
+const merge = require('webpack-merge')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const baseConfig = require('../../build/webpack.base.conf')
+const utils = require('../../build/utils')
+const webpack = require('webpack')
 // var projectRoot = path.resolve(__dirname, '../../')
 
-var webpackConfig = merge(baseConfig, {
+const webpackConfig = merge(baseConfig, {
   // use inline sourcemap for karma-sourcemap-loader
   module: {
     rules: utils.styleLoaders()
@@ -63,7 +63,7 @@ module.exports = function (config) {
     frameworks: ['mocha', 'sinon-chai'],
     reporters: ['mocha'],
     customLaunchers: {
-      'FirefoxHeadless': {
+      FirefoxHeadless: {
         base: 'Firefox',
         flags: [
           '-headless'
diff --git a/test/unit/specs/components/emoji_input.spec.js b/test/unit/specs/components/emoji_input.spec.js
index 6188308e..752111ef 100644
--- a/test/unit/specs/components/emoji_input.spec.js
+++ b/test/unit/specs/components/emoji_input.spec.js
@@ -29,7 +29,7 @@ const generateInput = (value, padEmoji = true) => {
       modelValue: value
     },
     slots: {
-      'default': () => h('input', '')
+      default: () => h('input', '')
     }
   })
   return wrapper
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 8a18a08b..616df6a0 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -4,7 +4,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
 const attentions = []
 const global = {
   mocks: {
-    '$store': {
+    $store: {
       state: {},
       getters: {
         mergedConfig: () => ({
diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js
index 584758f7..0fbab722 100644
--- a/test/unit/specs/components/user_profile.spec.js
+++ b/test/unit/specs/components/user_profile.spec.js
@@ -163,7 +163,7 @@ const localProfileStore = createStore({
       currentUser: {
         credentials: ''
       },
-      usersObject: { 100: localUser, 'testuser': localUser },
+      usersObject: { 100: localUser, testuser: localUser },
       users: [localUser],
       relationships: {}
     }
@@ -175,7 +175,7 @@ describe.skip('UserProfile', () => {
   it('renders external profile', () => {
     const wrapper = mount(UserProfile, {
       global: {
-        plugins: [ externalProfileStore ],
+        plugins: [externalProfileStore],
         mocks: {
           $route: {
             params: { id: 100 },
@@ -192,7 +192,7 @@ describe.skip('UserProfile', () => {
   it('renders local profile', () => {
     const wrapper = mount(UserProfile, {
       global: {
-        plugins: [ localProfileStore ],
+        plugins: [localProfileStore],
         mocks: {
           $route: {
             params: { name: 'testUser' },
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index b790b231..a8d0e5a3 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -245,7 +245,7 @@ describe('Statuses module', () => {
     it('increments count in existing reaction', () => {
       const state = defaultState()
       const status = makeMockStatus({ id: '1' })
-      status.emoji_reactions = [ { name: '😂', count: 1, accounts: [] } ]
+      status.emoji_reactions = [{ name: '😂', count: 1, accounts: [] }]
 
       mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
       mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
@@ -269,7 +269,7 @@ describe('Statuses module', () => {
     it('decreases count in existing reaction', () => {
       const state = defaultState()
       const status = makeMockStatus({ id: '1' })
-      status.emoji_reactions = [ { name: '😂', count: 2, accounts: [{ id: 'me' }] } ]
+      status.emoji_reactions = [{ name: '😂', count: 2, accounts: [{ id: 'me' }] }]
 
       mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
       mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js
index fbbca436..a42269b4 100644
--- a/test/unit/specs/services/chat_service/chat_service.spec.js
+++ b/test/unit/specs/services/chat_service/chat_service.spec.js
@@ -24,23 +24,23 @@ describe('chatService', () => {
   describe('.add', () => {
     it("Doesn't add duplicates", () => {
       const chat = chatService.empty()
-      chatService.add(chat, { messages: [ message1 ] })
-      chatService.add(chat, { messages: [ message1 ] })
+      chatService.add(chat, { messages: [message1] })
+      chatService.add(chat, { messages: [message1] })
       expect(chat.messages.length).to.eql(1)
 
-      chatService.add(chat, { messages: [ message2 ] })
+      chatService.add(chat, { messages: [message2] })
       expect(chat.messages.length).to.eql(2)
     })
 
     it('Updates minId and lastMessage and newMessageCount', () => {
       const chat = chatService.empty()
 
-      chatService.add(chat, { messages: [ message1 ] })
+      chatService.add(chat, { messages: [message1] })
       expect(chat.maxId).to.eql(message1.id)
       expect(chat.minId).to.eql(message1.id)
       expect(chat.newMessageCount).to.eql(1)
 
-      chatService.add(chat, { messages: [ message2 ] })
+      chatService.add(chat, { messages: [message2] })
       expect(chat.maxId).to.eql(message2.id)
       expect(chat.minId).to.eql(message1.id)
       expect(chat.newMessageCount).to.eql(2)
@@ -50,7 +50,7 @@ describe('chatService', () => {
       expect(chat.lastSeenMessageId).to.eql(message2.id)
 
       // Add message with higher id
-      chatService.add(chat, { messages: [ message3 ] })
+      chatService.add(chat, { messages: [message3] })
       expect(chat.newMessageCount).to.eql(1)
     })
   })
@@ -59,9 +59,9 @@ describe('chatService', () => {
     it('Updates minId and lastMessage', () => {
       const chat = chatService.empty()
 
-      chatService.add(chat, { messages: [ message1 ] })
-      chatService.add(chat, { messages: [ message2 ] })
-      chatService.add(chat, { messages: [ message3 ] })
+      chatService.add(chat, { messages: [message1] })
+      chatService.add(chat, { messages: [message2] })
+      chatService.add(chat, { messages: [message3] })
 
       expect(chat.maxId).to.eql(message3.id)
       expect(chat.minId).to.eql(message1.id)
@@ -80,9 +80,9 @@ describe('chatService', () => {
     it('Inserts date separators', () => {
       const chat = chatService.empty()
 
-      chatService.add(chat, { messages: [ message1 ] })
-      chatService.add(chat, { messages: [ message2 ] })
-      chatService.add(chat, { messages: [ message3 ] })
+      chatService.add(chat, { messages: [message1] })
+      chatService.add(chat, { messages: [message2] })
+      chatService.add(chat, { messages: [message3] })
 
       const view = chatService.getView(chat)
       expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message'])
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index 03fb32c9..525e57a5 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -314,8 +314,8 @@ describe('API Entities normalizer', () => {
       const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
       const result = parseLinkHeaderPagination(linkHeader)
       expect(result).to.eql({
-        'maxId': 861676,
-        'minId': 861741
+        maxId: 861676,
+        minId: 861741
       })
     })
 
@@ -323,8 +323,8 @@ describe('API Entities normalizer', () => {
       const linkHeader = '<http://example.com/api/v1/timelines/home?max_id=9waQx5IIS48qVue2Ai>; rel="next", <http://example.com/api/v1/timelines/home?min_id=9wi61nIPnfn674xgie>; rel="prev"'
       const result = parseLinkHeaderPagination(linkHeader, { flakeId: true })
       expect(result).to.eql({
-        'maxId': '9waQx5IIS48qVue2Ai',
-        'minId': '9wi61nIPnfn674xgie'
+        maxId: '9waQx5IIS48qVue2Ai',
+        minId: '9wi61nIPnfn674xgie'
       })
     })
   })
diff --git a/test/unit/specs/services/file_size_format/file_size_format.spec.js b/test/unit/specs/services/file_size_format/file_size_format.spec.js
index e02ac379..6804b6eb 100644
--- a/test/unit/specs/services/file_size_format/file_size_format.spec.js
+++ b/test/unit/specs/services/file_size_format/file_size_format.spec.js
@@ -25,8 +25,8 @@ describe('fileSizeFormat', () => {
       }
     ]
 
-    var res = []
-    for (var value in values) {
+    const res = []
+    for (const value in values) {
       res.push(fileSizeFormatService.fileSizeFormat(values[value]))
     }
     expect(res).to.eql(expected)
-- 
cgit v1.2.3-70-g09d2


From 5b7c6538745083dfd48771392ab22de557a7a344 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 2 Aug 2022 00:37:48 +0300
Subject: initial scratch

---
 src/App.js                                         |   1 +
 src/App.vue                                        |   1 +
 src/assets/pleromatan_apology.png                  | Bin 0 -> 405742 bytes
 src/assets/pleromatan_apology_fox.png              | Bin 0 -> 533320 bytes
 .../update_notification/update_notification.js     |  24 ++++++++++++
 .../update_notification/update_notification.scss   |  34 +++++++++++++++++
 .../update_notification/update_notification.vue    |  41 +++++++++++++++++++++
 src/i18n/en.json                                   |   9 +++++
 8 files changed, 110 insertions(+)
 create mode 100644 src/assets/pleromatan_apology.png
 create mode 100644 src/assets/pleromatan_apology_fox.png
 create mode 100644 src/components/update_notification/update_notification.js
 create mode 100644 src/components/update_notification/update_notification.scss
 create mode 100644 src/components/update_notification/update_notification.vue

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index d5967685..f5bd7e2e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -32,6 +32,7 @@ export default {
     MobileNav,
     DesktopNav,
     SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
+    UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
     UserReportingModal,
     PostStatusModal,
     GlobalNoticeList
diff --git a/src/App.vue b/src/App.vue
index 0efadaf0..c741aa70 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -65,6 +65,7 @@
     <UserReportingModal />
     <PostStatusModal />
     <SettingsModal />
+    <UpdateNotification />
     <div id="modal" />
     <GlobalNoticeList />
     <div id="popovers" />
diff --git a/src/assets/pleromatan_apology.png b/src/assets/pleromatan_apology.png
new file mode 100644
index 00000000..36ad7aeb
Binary files /dev/null and b/src/assets/pleromatan_apology.png differ
diff --git a/src/assets/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png
new file mode 100644
index 00000000..17f87694
Binary files /dev/null and b/src/assets/pleromatan_apology_fox.png differ
diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js
new file mode 100644
index 00000000..6b8665dd
--- /dev/null
+++ b/src/components/update_notification/update_notification.js
@@ -0,0 +1,24 @@
+import Modal from 'src/components/modal/modal.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import pleromaTan from 'src/assets/pleromatan_apology.png'
+import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png'
+
+import {
+  faTimes
+} from '@fortawesome/free-solid-svg-icons'
+library.add(
+  faTimes
+)
+
+const SettingsModal = {
+  data () {
+    return {
+      pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox
+    }
+  },
+  components: {
+    Modal
+  }
+}
+
+export default SettingsModal
diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss
new file mode 100644
index 00000000..5fd9eb9b
--- /dev/null
+++ b/src/components/update_notification/update_notification.scss
@@ -0,0 +1,34 @@
+@import 'src/_variables.scss';
+.UpdateNotificationModal {
+  /* Explanation:
+   * Modal is positioned vertically centered.
+   * 100vh - 100% = Distance between modal's top+bottom boundaries and screen
+   * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
+   * + 10% - we move modal completely off-screen, it's top boundary touches
+   *   bottom of the screen
+   * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
+   */
+  transform: translateY(calc(((100vh - 100%) / 2 + 5%)));
+  max-width: 90vh;
+  width: 30em;
+  position: relative;
+
+  @media all and (max-width: 800px) {
+    /* For mobile, the modal takes 100% of the available screen.
+    This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
+   */
+    transform: translateY(calc(100% - 50px));
+  }
+  .panel-body > p {
+    width: calc(100% - 10em)
+  }
+
+  .pleroma-tan {
+    max-width: 20em;
+    max-height: 40em;
+    position: absolute;
+    right: -5em;
+    top: -10em;
+    z-index: 10;
+  }
+}
diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue
new file mode 100644
index 00000000..e7118d81
--- /dev/null
+++ b/src/components/update_notification/update_notification.vue
@@ -0,0 +1,41 @@
+<template>
+  <Modal
+    :is-open="true"
+    class="UpdateNotification"
+    :class="{ peek: modalPeeked }"
+    :no-background="true"
+  >
+    <div class="UpdateNotificationModal panel">
+      <div class="panel-heading">
+        <span class="title">
+          {{ $t('update.big_update_title') }}
+        </span>
+      </div>
+      <div class="panel-body">
+        <p>
+          {{ $t('update.big_update_content') }}
+        </p>
+        <p>
+        <button
+          class="button-unstyled -link tall-status-hider"
+          @click.prevent="toggleShowMore"
+        >
+          {{ $t("general.show_more") }}
+        </button>
+        {{ ' ' }}
+        <button
+          class="button-unstyled -link tall-status-hider"
+          @click.prevent="toggleShowMore"
+        >
+          {{ $t("general.never_show_again") }}
+        </button>
+          </p>
+        <img class="pleroma-tan" :src="pleromaTanVariant"/>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<script src="./update_notification.js"></script>
+
+<style src="./update_notification.scss" lang="scss"></style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a10b271a..cd095ffb 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -71,6 +71,7 @@
     "optional": "optional",
     "show_more": "Show more",
     "show_less": "Show less",
+    "never_show_again": "Never show again",
     "dismiss": "Dismiss",
     "cancel": "Cancel",
     "disable": "Disable",
@@ -956,5 +957,13 @@
   },
   "display_date": {
     "today": "Today"
+  },
+  "update": {
+    "big_update_title": "Please bear with us",
+    "big_update_content": "We haven't had a release in a while, so there things might look and feel different than what you're used to.",
+    "update_bugs": "Please report any issues and bugs on {pleromaGitlab} we don't have a QA team so some things fall through the cracks, we're also open to feedback and suggestions.",
+    "update_bugs_gitlab": "Pleroma Gitlab",
+    "update_changelog": "You can see the full changelog {here}",
+    "update_changelog_here": "here"
   }
 }
-- 
cgit v1.2.3-70-g09d2


From d074aefb4ffe8fc7bdb0e5f0afec46f7042a90fe Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 17 Aug 2022 00:48:10 +0300
Subject: List edit UI overhaul

---
 src/App.js                                         |   4 +
 src/App.vue                                        |   2 +-
 src/components/lists_edit/lists_edit.js            |  80 +++++---
 src/components/lists_edit/lists_edit.vue           | 210 +++++++++++++++------
 .../lists_user_search/lists_user_search.js         |   7 +-
 .../lists_user_search/lists_user_search.vue        |  20 +-
 .../mobile_post_status_button.js                   |   3 +-
 src/components/tab_switcher/tab_switcher.scss      |   1 +
 src/i18n/en.json                                   |  13 +-
 9 files changed, 248 insertions(+), 92 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index f5bd7e2e..12a12f71 100644
--- a/src/App.js
+++ b/src/App.js
@@ -85,8 +85,12 @@ export default {
     isChats () {
       return this.$route.name === 'chat' || this.$route.name === 'chats'
     },
+    isListEdit () {
+      return this.$route.name === 'lists-edit'
+    },
     newPostButtonShown () {
       if (this.isChats) return false
+      if (this.isListEdit) return false
       return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
     },
     showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
diff --git a/src/App.vue b/src/App.vue
index c741aa70..cb6a0115 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -33,7 +33,7 @@
       <div
         id="main-scroller"
         class="column main"
-        :class="{ '-full-height': isChats }"
+        :class="{ '-full-height': isChats || isListEdit }"
       >
         <div
           v-if="!currentUser"
diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js
index e4d0c22a..b1516af0 100644
--- a/src/components/lists_edit/lists_edit.js
+++ b/src/components/lists_edit/lists_edit.js
@@ -1,7 +1,9 @@
 import { mapState, mapGetters } from 'vuex'
 import BasicUserCard from '../basic_user_card/basic_user_card.vue'
 import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
+import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faSearch,
@@ -17,22 +19,32 @@ const ListsNew = {
   components: {
     BasicUserCard,
     UserAvatar,
-    ListsUserSearch
+    ListsUserSearch,
+    TabSwitcher,
+    PanelLoading
   },
   data () {
     return {
       title: '',
-      userIds: [],
-      selectedUserIds: []
+      titleDraft: '',
+      membersUserIds: [],
+      removedUserIds: new Set([]), // users we added for members, to undo
+      searchUserIds: [],
+      addedUserIds: new Set([]), // users we added from search, to undo
+      searchLoading: false,
+      reallyDelete: false
     }
   },
   created () {
     this.$store.dispatch('fetchList', { listId: this.id })
-      .then(() => { this.title = this.findListTitle(this.id) })
+      .then(() => {
+        this.title = this.findListTitle(this.id)
+        this.titleDraft = this.title
+      })
     this.$store.dispatch('fetchListAccounts', { listId: this.id })
       .then(() => {
-        this.selectedUserIds = this.findListAccounts(this.id)
-        this.selectedUserIds.forEach(userId => {
+        this.membersUserIds = this.findListAccounts(this.id)
+        this.membersUserIds.forEach(userId => {
           this.$store.dispatch('fetchUserIfMissing', userId)
         })
       })
@@ -41,11 +53,12 @@ const ListsNew = {
     id () {
       return this.$route.params.id
     },
-    users () {
-      return this.userIds.map(userId => this.findUser(userId))
+    membersUsers () {
+      return [...this.membersUserIds, ...this.addedUserIds]
+        .map(userId => this.findUser(userId)).filter(user => user)
     },
-    selectedUsers () {
-      return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user)
+    searchUsers () {
+      return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
     },
     ...mapState({
       currentUser: state => state.users.currentUser
@@ -56,30 +69,51 @@ const ListsNew = {
     onInput () {
       this.search(this.query)
     },
-    selectUser (user) {
-      if (this.selectedUserIds.includes(user.id)) {
+    toggleRemoveMember (user) {
+      if (this.removedUserIds.has(user.id)) {
+        this.addUser(user)
+        this.removedUserIds.delete(user.id)
+      } else {
+        this.removeUser(user.id)
+        this.removedUserIds.add(user.id)
+      }
+    },
+    toggleAddFromSearch (user) {
+      if (this.addedUserIds.has(user.id)) {
         this.removeUser(user.id)
+        this.addedUserIds.delete(user.id)
       } else {
         this.addUser(user)
+        this.addedUserIds.add(user.id)
       }
     },
-    isSelected (user) {
-      return this.selectedUserIds.includes(user.id)
+    isRemoved (user) {
+      return this.removedUserIds.has(user.id)
+    },
+    isAdded (user) {
+      return this.addedUserIds.has(user.id)
     },
     addUser (user) {
-      this.selectedUserIds.push(user.id)
+      // this.selectedUserIds.push(user.id)
     },
     removeUser (userId) {
-      this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
+      // this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
     },
-    onResults (results) {
-      this.userIds = results
+    onSearchLoading (results) {
+      this.searchLoading = true
     },
-    updateList () {
-      this.$store.dispatch('setList', { listId: this.id, title: this.title })
-      this.$store.dispatch('setListAccounts', { listId: this.id, accountIds: this.selectedUserIds })
-
-      this.$router.push({ name: 'lists-timeline', params: { id: this.id } })
+    onSearchLoadingDone (results) {
+      this.searchLoading = false
+    },
+    onSearchResults (results) {
+      this.searchLoading = false
+      this.searchUserIds = results
+    },
+    updateListTitle () {
+      this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft })
+        .then(() => {
+          this.title = this.findListTitle(this.id)
+        })
     },
     deleteList () {
       this.$store.dispatch('deleteList', { listId: this.id })
diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue
index 69007b02..dffbff2f 100644
--- a/src/components/lists_edit/lists_edit.vue
+++ b/src/components/lists_edit/lists_edit.vue
@@ -1,8 +1,8 @@
 <template>
-  <div class="panel-default panel list-edit">
+  <div class="panel-default panel ListEdit">
     <div
       ref="header"
-      class="panel-heading"
+      class="panel-heading list-edit-heading"
     >
       <button
         class="button-unstyled go-back-button"
@@ -13,54 +13,129 @@
           icon="chevron-left"
         />
       </button>
-    </div>
-    <div class="input-wrap">
-      <input
-        ref="title"
-        v-model="title"
-        :placeholder="$t('lists.title')"
+      <div class="title">
+      <i18n-t
+        keypath="lists.editing_list"
       >
+        <template #listTitle>{{ title }}</template>
+      </i18n-t>
+      </div>
     </div>
-    <div class="member-list">
-      <div
-        v-for="user in selectedUsers"
-        :key="user.id"
-        class="member"
-      >
-        <BasicUserCard
-          :user="user"
-          :class="isSelected(user) ? 'selected' : ''"
-          @click.capture.prevent="selectUser(user)"
-        />
+    <div class="panel-body">
+      <div class="input-wrap">
+        <label for="list-edit-title">{{ $t('lists.title') }}</label>
+        {{ ' ' }}
+        <input
+          id="list-edit-title"
+          ref="title"
+          v-model="titleDraft"
+        >
+        <button
+          class="btn button-default follow-button"
+          @click="updateListTitle"
+        >
+          {{ $t('lists.update_title') }}
+        </button>
       </div>
+      <tab-switcher
+        class="list-member-management"
+        :scrollable-tabs="true"
+        >
+        <div
+          :label="$t('lists.manage_members')"
+          class="members-list"
+          >
+          <div class="users-list">
+            <div
+              v-for="user in membersUsers"
+              :key="user.id"
+              class="member"
+            >
+              <BasicUserCard
+                :user="user"
+              >
+                <button
+                  class="btn button-default follow-button"
+                  @click="toggleRemoveMember(user)"
+                >
+                  {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
+                </button>
+              </BasicUserCard>
+            </div>
+          </div>
+        </div>
+
+        <div
+          class="search-list"
+          :label="$t('lists.add_members')"
+          >
+          <ListsUserSearch
+            @results="onSearchResults"
+            @loading="onSearchLoading"
+            @loadingDone="onSearchLoadingDone"
+          />
+          <div v-if="searchLoading" class="loading">
+            <PanelLoading />
+          </div>
+          <div v-else class="users-list">
+            <div
+              v-for="user in searchUsers"
+              :key="user.id"
+              class="member"
+            >
+              <BasicUserCard
+                :user="user"
+              >
+                <span
+                  v-if="membersUserIds.includes(user.id)"
+                >
+                  {{ $t('lists.is_in_list') }}
+                </span>
+                <button
+                  v-if="!membersUserIds.includes(user.id)"
+                  class="btn button-default follow-button"
+                  @click="toggleAddFromSearch(user)"
+                >
+                  {{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }}
+                </button>
+                <button
+                  v-else
+                  class="btn button-default follow-button"
+                  @click="toggleRemoveMember(user)"
+                >
+                  {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
+                </button>
+              </BasicUserCard>
+            </div>
+          </div>
+        </div>
+      </tab-switcher>
     </div>
-    <ListsUserSearch @results="onResults" />
-    <div class="member-list">
-      <div
-        v-for="user in users"
-        :key="user.id"
-        class="member"
+    <div class="panel-footer">
+      <span class="spacer" />
+      <button
+        class="btn button-default delete-button"
+        @click="reallyDelete = true"
+        v-if="!reallyDelete"
       >
-        <BasicUserCard
-          :user="user"
-          :class="isSelected(user) ? 'selected' : ''"
-          @click.capture.prevent="selectUser(user)"
-        />
-      </div>
+        {{ $t('lists.delete') }}
+      </button>
+      <template v-else>
+        {{ $t('lists.really_delete') }}
+        <button
+          class="btn button-default delete-button"
+          @click="deleteList"
+        >
+        {{ $t('general.yes') }}
+        </button>
+        <button
+          class="btn button-default delete-button"
+          @click="reallyDelete = false"
+        >
+        {{ $t('general.no') }}
+        </button>
+      </template>
     </div>
-    <button
-      :disabled="title && title.length === 0"
-      class="btn button-default"
-      @click="updateList"
-    >
-      {{ $t('lists.save') }}
-    </button>
-    <button
-      class="btn button-default"
-      @click="deleteList"
-    >
-      {{ $t('lists.delete') }}
-    </button>
   </div>
 </template>
 
@@ -69,28 +144,43 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
-.list-edit {
-  .input-wrap {
+.ListEdit {
+  --panel-body-padding: 0.5em;
+
+  height: calc(100vh - var(--navbar-height));
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+
+  .list-edit-heading {
+    grid-template-columns: auto minmax(50%, 1fr);
+  }
+
+  .panel-body {
     display: flex;
-    margin: 0.7em 0.5em 0.7em 0.5em;
+    flex: 1;
+    flex-direction: column;
+    overflow: hidden;
+  }
 
-    input {
-      width: 100%;
-    }
+  .list-member-management {
+    flex: 1 0 auto;
   }
 
   .search-icon {
     margin-right: 0.3em;
   }
 
-  .member-list {
+  .users-list {
     padding-bottom: 0.7rem;
+    overflow-y: auto;
   }
 
-  .basic-user-card:hover,
-  .basic-user-card.selected {
-    cursor: pointer;
-    background-color: var(--selectedPost, $fallback--lightBg);
+  & .search-list,
+  & .members-list {
+    overflow: hidden;
+    flex-direction: column;
+    min-height: 0;
   }
 
   .go-back-button {
@@ -102,7 +192,15 @@
   }
 
   .btn {
-    margin: 0.5em;
+    margin: 0 0.5em;
+  }
+
+  .panel-footer {
+    grid-template-columns: minmax(10%, 1fr);
+
+    .delete-button {
+      min-width: 9em;
+    }
   }
 }
 </style>
diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js
index 5798841a..c92ec0ee 100644
--- a/src/components/lists_user_search/lists_user_search.js
+++ b/src/components/lists_user_search/lists_user_search.js
@@ -15,6 +15,7 @@ const ListsUserSearch = {
   components: {
     Checkbox
   },
+  emits: ['loading', 'loadingDone', 'results'],
   data () {
     return {
       loading: false,
@@ -33,12 +34,16 @@ const ListsUserSearch = {
       }
 
       this.loading = true
+      this.$emit('loading')
       this.userIds = []
       this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
         .then(data => {
-          this.loading = false
           this.$emit('results', data.accounts.map(a => a.id))
         })
+        .finally(() => {
+          this.loading = false
+          this.$emit('loadingDone')
+        })
     }
   }
 }
diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue
index 03fb3ba6..8633170c 100644
--- a/src/components/lists_user_search/lists_user_search.vue
+++ b/src/components/lists_user_search/lists_user_search.vue
@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="ListsUserSearch">
     <div class="input-wrap">
       <div class="input-search">
         <FAIcon
@@ -29,17 +29,19 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
-.input-wrap {
-  display: flex;
-  margin: 0.7em 0.5em 0.7em 0.5em;
+.ListsUserSearch {
+  .input-wrap {
+    display: flex;
+    margin: 0.7em 0.5em 0.7em 0.5em;
 
-  input {
-    width: 100%;
+    input {
+      width: 100%;
+    }
   }
-}
 
-.search-icon {
-  margin-right: 0.3em;
+  .search-icon {
+    margin-right: 0.3em;
+  }
 }
 
 </style>
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 ecf79b64..f7f96cd6 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
@@ -10,7 +10,8 @@ library.add(
 
 const HIDDEN_FOR_PAGES = new Set([
   'chats',
-  'chat'
+  'chat',
+  'lists-edit'
 ])
 
 const MobilePostStatusButton = {
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 7a086b26..d930368c 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -17,6 +17,7 @@
       overflow-x: auto;
       padding-top: 5px;
       flex-direction: row;
+      flex: 0 0 auto;
 
       &::after, &::before {
         content: '';
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7352b75c..733bd7ac 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -80,6 +80,9 @@
     "confirm": "Confirm",
     "verify": "Verify",
     "close": "Close",
+    "undo": "Undo",
+    "yes": "Yes",
+    "no": "No",
     "peek": "Peek",
     "role": {
       "admin": "Admin",
@@ -981,7 +984,15 @@
     "save": "Save changes",
     "delete": "Delete list",
     "following_only": "Limit to Following",
-    "manage_lists": "Manage lists"
+    "manage_lists": "Manage lists",
+    "manage_members": "Manage list members",
+    "add_members": "Search for more users",
+    "remove_from_list": "Remove from list",
+    "add_to_list": "Add to list",
+    "is_in_list": "Already in list",
+    "editing_list": "Editing list {listTitle}",
+    "update_title": "Save Title",
+    "really_delete": "Really delete list?"
   },
   "file_type": {
     "audio": "Audio",
-- 
cgit v1.2.3-70-g09d2


From a29835375a62549410a7df7922f8eb3f9b391487 Mon Sep 17 00:00:00 2001
From: Alexander Tumin <iamtakingiteasy@eientei.org>
Date: Wed, 17 Aug 2022 02:33:39 +0300
Subject: Allow column width configuration: allow stretching navbar with
 columns

---
 src/App.js                                         |  7 +++++++
 src/App.scss                                       | 15 ++++++++-------
 src/App.vue                                        |  5 ++++-
 src/components/desktop_nav/desktop_nav.scss        | 20 ++++++++++++++++++++
 src/components/settings_modal/tabs/general_tab.vue |  5 +++++
 src/i18n/en.json                                   |  1 +
 src/modules/config.js                              |  1 +
 7 files changed, 46 insertions(+), 8 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.js b/src/App.js
index f5bd7e2e..d1ad16d5 100644
--- a/src/App.js
+++ b/src/App.js
@@ -60,6 +60,13 @@ export default {
         '-' + this.layoutType
       ]
     },
+    navClasses () {
+      const { navbarColumnStretch } = this.$store.getters.mergedConfig
+      return [
+        '-' + this.layoutType,
+        ...(navbarColumnStretch ? ['-column-stretch'] : [])
+      ]
+    },
     currentUser () { return this.$store.state.users.currentUser },
     userBackground () { return this.currentUser.background_image },
     instanceBackground () {
diff --git a/src/App.scss b/src/App.scss
index 3c16007e..02f5e049 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -185,13 +185,14 @@ nav {
   --maxiColumn: 45rem;
   --columnGap: 1em;
   --status-margin: 0.75em;
-
+  --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
+  --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
   --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));
 
   position: relative;
   display: grid;
   grid-template-columns:
-    minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)))
+    var(--effectiveSidebarColumnWidth)
     var(--effectiveContentColumnWidth);
   grid-template-areas: "sidebar content";
   grid-template-rows: 1fr;
@@ -288,22 +289,22 @@ nav {
   &.-reverse:not(.-wide):not(.-mobile) {
     grid-template-columns:
       var(--effectiveContentColumnWidth)
-      minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
+      var(--effectiveSidebarColumnWidth);
     grid-template-areas: "content sidebar";
   }
 
   &.-wide {
     grid-template-columns:
-      minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)))
+      var(--effectiveSidebarColumnWidth)
       var(--effectiveContentColumnWidth)
-      minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
+      var(--effectiveNotifsColumnWidth);
     grid-template-areas: "sidebar content notifs";
 
     &.-reverse {
       grid-template-columns:
-        minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)))
+        var(--effectiveNotifsColumnWidth)
         var(--effectiveContentColumnWidth)
-        minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
+        var(--effectiveSidebarColumnWidth);
       grid-template-areas: "notifs content sidebar";
     }
   }
diff --git a/src/App.vue b/src/App.vue
index c741aa70..1f96efe8 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,7 +8,10 @@
       class="app-bg-wrapper"
     />
     <MobileNav v-if="layoutType === 'mobile'" />
-    <DesktopNav v-else />
+    <DesktopNav
+      v-else
+      :class="navClasses"
+    />
     <Notifications v-if="currentUser" />
     <div
       id="content"
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index 71202244..f6c7c43d 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -23,6 +23,26 @@
     max-width: 980px;
   }
 
+  &.-column-stretch .inner-nav {
+    --miniColumn: 25rem;
+    --maxiColumn: 45rem;
+    --columnGap: 1em;
+    max-width: calc(
+      var(--sidebarColumnWidth, var(--miniColumn)) +
+      var(--contentColumnWidth, var(--maxiColumn)) +
+      var(--columnGap)
+    );
+  }
+
+  &.-column-stretch.-wide .inner-nav {
+    max-width: calc(
+      var(--sidebarColumnWidth, var(--miniColumn)) +
+      var(--contentColumnWidth, var(--maxiColumn)) +
+      var(--notifsColumnWidth, var(--miniColumn)) +
+      var(--columnGap)
+    );
+  }
+
   &.-logoLeft .inner-nav {
     grid-template-columns: auto 2fr 2fr;
     grid-template-areas: "logo sitename actions";
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index db321363..333b5493 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -122,6 +122,11 @@
             {{ $t('settings.right_sidebar') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="navbarColumnStretch">
+            {{ $t('settings.navbar_column_stretch') }}
+          </BooleanSetting>
+        </li>
         <li>
           <ChoiceSetting
             v-if="user"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7e957ffd..a5d3cec6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -412,6 +412,7 @@
     "hide_isp": "Hide instance-specific panel",
     "hide_shoutbox": "Hide instance shoutbox",
     "right_sidebar": "Reverse order of columns",
+    "navbar_column_stretch": "Stretch navbar to columns width",
     "always_show_post_button": "Always show floating New Post button",
     "hide_wallpaper": "Hide instance wallpaper",
     "preload_images": "Preload images",
diff --git a/src/modules/config.js b/src/modules/config.js
index 2918f865..1c2a45c2 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -87,6 +87,7 @@ export const defaultState = {
   sidebarColumnWidth: '25rem',
   contentColumnWidth: '45rem',
   notifsColumnWidth: '25rem',
+  navbarColumnStretch: false,
   listsNavigation: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
-- 
cgit v1.2.3-70-g09d2


From 3dd9f6f68ce5625018a711ca78aa5a2a31441433 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 9 Oct 2022 23:01:49 +0300
Subject: moved popovers space outside app because otherwise it causes weird
 issues A LOT

---
 index.html  | 1 +
 src/App.vue | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/App.vue')

diff --git a/index.html b/index.html
index ba072eda..4af84a59 100644
--- a/index.html
+++ b/index.html
@@ -10,5 +10,6 @@
     <noscript>To use Pleroma, please enable JavaScript.</noscript>
     <div id="app"></div>
     <!-- built files will be auto injected -->
+    <div id="popovers" />
   </body>
 </html>
diff --git a/src/App.vue b/src/App.vue
index e0d709f7..060fa70a 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -73,7 +73,6 @@
     <UpdateNotification />
     <div id="modal" />
     <GlobalNoticeList />
-    <div id="popovers" />
   </div>
 </template>
 
-- 
cgit v1.2.3-70-g09d2


From f0ba0e68559b3665b76f0022c5ad3b8f457c37a5 Mon Sep 17 00:00:00 2001
From: tusooa <tusooa@kazv.moe>
Date: Mon, 7 Nov 2022 12:49:41 -0500
Subject: Use <main> for main content

---
 src/App.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index e0d709f7..8dad8ce9 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -33,7 +33,7 @@
           <div id="notifs-sidebar" />
         </template>
       </div>
-      <div
+      <main
         id="main-scroller"
         class="column main"
         :class="{ '-full-height': isChats || isListEdit }"
@@ -50,7 +50,7 @@
           </router-link>
         </div>
         <router-view />
-      </div>
+      </main>
       <div
         id="notifs-column"
         class="column -scrollable"
-- 
cgit v1.2.3-70-g09d2


From 771933183be07d84f06103338f8b6b7c4ed282da Mon Sep 17 00:00:00 2001
From: tusooa <tusooa@kazv.moe>
Date: Tue, 8 Nov 2022 00:23:08 -0500
Subject: Make user panel and notification <aside>

---
 src/App.vue                              | 2 +-
 src/components/mobile_nav/mobile_nav.vue | 4 ++--
 src/components/user_panel/user_panel.vue | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 8dad8ce9..12b4f61b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -30,7 +30,7 @@
           <instance-specific-panel v-if="showInstanceSpecificPanel" />
           <features-panel v-if="!currentUser && showFeaturesPanel" />
           <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
-          <div id="notifs-sidebar" />
+          <aside id="notifs-sidebar" />
         </template>
       </div>
       <main
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index ee360f80..6e732d1f 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -42,7 +42,7 @@
         </button>
       </div>
     </nav>
-    <div
+    <aside
       v-if="currentUser"
       class="mobile-notifications-drawer"
       :class="{ '-closed': !notificationsOpen }"
@@ -83,7 +83,7 @@
         ref="mobileNotifications"
         @scroll="onScroll"
       />
-    </div>
+    </aside>
     <SideDrawer
       ref="sideDrawer"
       :logout="logout"
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 243de387..95ec97af 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="user-panel">
+  <aside class="user-panel">
     <div
       v-if="signedIn"
       key="user-panel-signed"
@@ -16,7 +16,7 @@
       v-else
       key="user-panel"
     />
-  </div>
+  </aside>
 </template>
 
 <script src="./user_panel.js"></script>
-- 
cgit v1.2.3-70-g09d2


From a34bea75a808de9841f616896ebb630972475707 Mon Sep 17 00:00:00 2001
From: tusooa <tusooa@kazv.moe>
Date: Tue, 8 Nov 2022 00:37:22 -0500
Subject: Handle properly 2-col and 3-col desktop notifications

---
 src/App.vue                                    | 2 +-
 src/components/notifications/notifications.vue | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index 12b4f61b..8dad8ce9 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -30,7 +30,7 @@
           <instance-specific-panel v-if="showInstanceSpecificPanel" />
           <features-panel v-if="!currentUser && showFeaturesPanel" />
           <who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
-          <aside id="notifs-sidebar" />
+          <div id="notifs-sidebar" />
         </template>
       </div>
       <main
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index f9c90a7e..7ed419d1 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -3,7 +3,8 @@
     :disabled="minimalMode || disableTeleport"
     :to="teleportTarget"
   >
-    <div
+    <component
+      :is="noHeading ? 'div' : 'aside'"
       ref="root"
       :class="{ minimal: minimalMode }"
       class="Notifications"
@@ -88,7 +89,7 @@
           </div>
         </div>
       </div>
-    </div>
+    </component>
   </teleport>
 </template>
 
-- 
cgit v1.2.3-70-g09d2

