From bfb3a4364be3f71f7c46056aeb8972f04a072703 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 3 Feb 2022 22:34:57 +0200
Subject: options to disable (You)s and highlighting of yourself

---
 src/components/settings_modal/tabs/general_tab.vue | 10 ++++++++++
 1 file changed, 10 insertions(+)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 44b1ac92..eba3b268 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -147,6 +147,11 @@
             {{ $t('settings.greentext') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="mentionLinkShowYous">
+            {{ $t('settings.show_yous') }}
+          </BooleanSetting>
+        </li>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
@@ -181,6 +186,11 @@
               {{ $t('settings.mention_link_fade_domain') }}
             </BooleanSetting>
           </li>
+          <li>
+            <BooleanSetting path="mentionLinkBoldenYou">
+              {{ $t('settings.mention_link_bolden_you') }}
+            </BooleanSetting>
+          </li>
         </ul>
       </ul>
     </div>
-- 
cgit v1.2.3-70-g09d2


From 9c1814d12243f45cb67a797780a8c393f301080c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 22 Feb 2022 23:31:40 +0200
Subject: expert settings toggle + server-side settings

---
 .../settings_modal/helpers/boolean_setting.js      |  15 ++-
 .../settings_modal/helpers/boolean_setting.vue     |   2 +
 .../settings_modal/helpers/choice_setting.js       |   8 +-
 .../settings_modal/helpers/choice_setting.vue      |   2 +
 .../helpers/server_side_indicator.vue              |  51 +++++++++
 .../helpers/shared_computed_object.js              |   9 ++
 src/components/settings_modal/settings_modal.js    |  11 ++
 src/components/settings_modal/settings_modal.scss  |   7 ++
 src/components/settings_modal/settings_modal.vue   |   6 +-
 .../settings_modal/tabs/filtering_tab.vue          |  46 +-------
 src/components/settings_modal/tabs/general_tab.js  |  11 +-
 src/components/settings_modal/tabs/general_tab.vue | 122 +++++++++++---------
 .../settings_modal/tabs/notifications_tab.js       |   8 +-
 .../settings_modal/tabs/notifications_tab.vue      |  72 +++++++++---
 src/components/settings_modal/tabs/profile_tab.js  |  23 +---
 src/components/settings_modal/tabs/profile_tab.vue | 124 ++++++++++-----------
 src/i18n/en.json                                   |   5 +
 src/main.js                                        |   2 +
 src/modules/config.js                              |   1 +
 src/modules/serverSideConfig.js                    | 111 ++++++++++++++++++
 .../entity_normalizer/entity_normalizer.service.js |   1 +
 21 files changed, 433 insertions(+), 204 deletions(-)
 create mode 100644 src/components/settings_modal/helpers/server_side_indicator.vue
 create mode 100644 src/modules/serverSideConfig.js

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js
index 5c52f697..353e551c 100644
--- a/src/components/settings_modal/helpers/boolean_setting.js
+++ b/src/components/settings_modal/helpers/boolean_setting.js
@@ -1,14 +1,17 @@
 import { get, set } from 'lodash'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
 import ModifiedIndicator from './modified_indicator.vue'
+import ServerSideIndicator from './server_side_indicator.vue'
 export default {
   components: {
     Checkbox,
-    ModifiedIndicator
+    ModifiedIndicator,
+    ServerSideIndicator
   },
   props: [
     'path',
-    'disabled'
+    'disabled',
+    'expert'
   ],
   computed: {
     pathDefault () {
@@ -26,8 +29,14 @@ export default {
     defaultState () {
       return get(this.$parent, this.pathDefault)
     },
+    isServerSide () {
+      return this.path.startsWith('serverSide_')
+    },
     isChanged () {
-      return this.state !== this.defaultState
+      return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
+    },
+    matchesExpertLevel () {
+      return (this.expert || 0) <= this.$parent.expertLevel
     }
   },
   methods: {
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index c3ee6583..c82cf23f 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -1,6 +1,7 @@
 <template>
   <label
     class="BooleanSetting"
+    v-if="matchesExpertLevel"
   >
     <Checkbox
       :checked="state"
@@ -14,6 +15,7 @@
         <slot />
       </span>
       <ModifiedIndicator :changed="isChanged" />
+      <ServerSideIndicator :serverSide="isServerSide" />
     </Checkbox>
   </label>
 </template>
diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js
index a15f6bac..07d0f76d 100644
--- a/src/components/settings_modal/helpers/choice_setting.js
+++ b/src/components/settings_modal/helpers/choice_setting.js
@@ -9,7 +9,8 @@ export default {
   props: [
     'path',
     'disabled',
-    'options'
+    'options',
+    'expert'
   ],
   computed: {
     pathDefault () {
@@ -28,7 +29,10 @@ export default {
       return get(this.$parent, this.pathDefault)
     },
     isChanged () {
-      return this.state !== this.defaultState
+      return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
+    },
+    matchesExpertLevel () {
+      return (this.expert || 0) <= this.$parent.expertLevel
     }
   },
   methods: {
diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue
index fa17661b..845886ca 100644
--- a/src/components/settings_modal/helpers/choice_setting.vue
+++ b/src/components/settings_modal/helpers/choice_setting.vue
@@ -1,6 +1,7 @@
 <template>
   <label
     class="ChoiceSetting"
+    v-if="matchesExpertLevel"
   >
     <slot />
     <Select
@@ -18,6 +19,7 @@
       </option>
     </Select>
     <ModifiedIndicator :changed="isChanged" />
+    <ServerSideIndicator :serverSide="isServerSide" />
   </label>
 </template>
 
diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/server_side_indicator.vue
new file mode 100644
index 00000000..143a86a1
--- /dev/null
+++ b/src/components/settings_modal/helpers/server_side_indicator.vue
@@ -0,0 +1,51 @@
+<template>
+  <span
+    v-if="serverSide"
+    class="ServerSideIndicator"
+  >
+    <Popover
+      trigger="hover"
+    >
+      <template v-slot:trigger>
+        &nbsp;
+        <FAIcon
+          icon="server"
+          :aria-label="$t('settings.setting_server_side')"
+        />
+      </template>
+      <template v-slot:content>
+        <div class="serverside-tooltip">
+          {{ $t('settings.setting_server_side') }}
+        </div>
+      </template>
+    </Popover>
+  </span>
+</template>
+
+<script>
+import Popover from 'src/components/popover/popover.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faServer } from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faServer
+)
+
+export default {
+  components: { Popover },
+  props: ['serverSide']
+}
+</script>
+
+<style lang="scss">
+.ServerSideIndicator {
+  display: inline-block;
+  position: relative;
+
+  .serverside-tooltip {
+    margin: 0.5em 1em;
+    min-width: 10em;
+    text-align: center;
+  }
+}
+</style>
diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
index 2c833c0c..12431dca 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -1,4 +1,5 @@
 import { defaultState as configDefaultState } from 'src/modules/config.js'
+import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
 
 const SharedComputedObject = () => ({
   user () {
@@ -22,6 +23,14 @@ const SharedComputedObject = () => ({
       }
     }])
     .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  ...Object.keys(serverSideConfigDefaultState)
+    .map(key => ['serverSide_' + key, {
+      get () { return this.$store.state.serverSideConfig[key] },
+      set (value) {
+        this.$store.dispatch('setServerSideOption', { name: key, value })
+      }
+    }])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
   // Special cases (need to transform values or perform actions first)
   useStreamingApi: {
     get () { return this.$store.getters.mergedConfig.useStreamingApi },
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 04043483..82ea410e 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -3,6 +3,7 @@ import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
 import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
 import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
 import Popover from '../popover/popover.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import { cloneDeep } from 'lodash'
 import {
@@ -51,6 +52,7 @@ const SettingsModal = {
   components: {
     Modal,
     Popover,
+    Checkbox,
     SettingsModalContent: getResettableAsyncComponent(
       () => import('./settings_modal_content.vue'),
       {
@@ -159,6 +161,15 @@ const SettingsModal = {
     },
     modalPeeked () {
       return this.$store.state.interface.settingsModalState === 'minimized'
+    },
+    expertLevel: {
+      get () {
+        return this.$store.state.config.expertLevel > 0
+      },
+      set (value) {
+        console.log(value)
+        this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
+      }
     }
   }
 }
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 90446b36..fb466f2f 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -48,4 +48,11 @@
       }
     }
   }
+
+  .settings-footer {
+    display: flex;
+    >* {
+      margin-right: 0.5em;
+    }
+  }
 }
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 583c2ecc..0aad1abb 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -53,7 +53,7 @@
       <div class="panel-body">
         <SettingsModalContent v-if="modalOpenedOnce" />
       </div>
-      <div class="panel-footer">
+      <div class="panel-footer settings-footer">
         <Popover
           class="export"
           trigger="click"
@@ -108,6 +108,10 @@
             </div>
           </template>
         </Popover>
+
+        <Checkbox v-model="expertLevel" >
+          {{ $t("settings.expert_mode")}}
+        </Checkbox>
       </div>
     </div>
   </Modal>
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 50ee20e0..cd7f0bc4 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -38,7 +38,7 @@
           </ul>
         </li>
         <li>
-          <BooleanSetting path="hidePostStats">
+          <BooleanSetting path="hidePostStats" expert="1">
             {{ $t('settings.hide_post_stats') }}
           </BooleanSetting>
         </li>
@@ -59,7 +59,7 @@
           <div>{{ $t('settings.filtering_explanation') }}</div>
         </li>
         <h3>{{ $t('settings.attachments') }}</h3>
-        <li>
+        <li v-if="expertLevel > 0">
           <label for="maxThumbnails">
             {{ $t('settings.max_thumbnails') }}
           </label>
@@ -84,7 +84,7 @@
         </li>
       </ul>
     </div>
-    <div class="setting-item">
+    <div class="setting-item" v-if="expertLevel > 0">
       <h2>{{ $t('settings.user_profiles') }}</h2>
       <ul class="setting-list">
         <li>
@@ -94,46 +94,6 @@
         </li>
       </ul>
     </div>
-    <div class="setting-item">
-      <h2>{{ $t('settings.notifications') }}</h2>
-      <ul class="setting-list">
-        <li class="select-multiple">
-          <span class="label">{{ $t('settings.notification_visibility') }}</span>
-          <ul class="option-list">
-            <li>
-              <BooleanSetting path="notificationVisibility.likes">
-                {{ $t('settings.notification_visibility_likes') }}
-              </BooleanSetting>
-            </li>
-            <li>
-              <BooleanSetting path="notificationVisibility.repeats">
-                {{ $t('settings.notification_visibility_repeats') }}
-              </BooleanSetting>
-            </li>
-            <li>
-              <BooleanSetting path="notificationVisibility.follows">
-                {{ $t('settings.notification_visibility_follows') }}
-              </BooleanSetting>
-            </li>
-            <li>
-              <BooleanSetting path="notificationVisibility.mentions">
-                {{ $t('settings.notification_visibility_mentions') }}
-              </BooleanSetting>
-            </li>
-            <li>
-              <BooleanSetting path="notificationVisibility.moves">
-                {{ $t('settings.notification_visibility_moves') }}
-              </BooleanSetting>
-            </li>
-            <li>
-              <BooleanSetting path="notificationVisibility.emojiReactions">
-                {{ $t('settings.notification_visibility_emoji_reactions') }}
-              </BooleanSetting>
-            </li>
-          </ul>
-        </li>
-      </ul>
-    </div>
   </div>
 </template>
 <script src="./filtering_tab.js"></script>
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 952c328d..9e4e282f 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -1,8 +1,10 @@
 import BooleanSetting from '../helpers/boolean_setting.vue'
 import ChoiceSetting from '../helpers/choice_setting.vue'
+import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
 import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
 
 import SharedComputedObject from '../helpers/shared_computed_object.js'
+import ServerSideIndicator from '../helpers/server_side_indicator.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faGlobe
@@ -37,7 +39,9 @@ const GeneralTab = {
   components: {
     BooleanSetting,
     ChoiceSetting,
-    InterfaceLanguageSwitcher
+    InterfaceLanguageSwitcher,
+    ScopeSelector,
+    ServerSideIndicator,
   },
   computed: {
     postFormats () {
@@ -57,6 +61,11 @@ const GeneralTab = {
     },
     instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
     ...SharedComputedObject()
+  },
+  methods: {
+    changeDefaultScope (value) {
+      this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
+    }
   }
 }
 
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index eba3b268..4accf0c1 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -27,7 +27,7 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="streaming">
+          <BooleanSetting path="streaming" expert="1">
             {{ $t('settings.streaming') }}
           </BooleanSetting>
           <ul
@@ -38,6 +38,7 @@
               <BooleanSetting
                 path="pauseOnUnfocused"
                 :disabled="!streaming"
+                expert="1"
               >
                 {{ $t('settings.pause_on_unfocused') }}
               </BooleanSetting>
@@ -45,7 +46,7 @@
           </ul>
         </li>
         <li>
-          <BooleanSetting path="useStreamingApi">
+          <BooleanSetting path="useStreamingApi" expert="1">
             {{ $t('settings.useStreamingApi') }}
             <br>
             <small>
@@ -54,17 +55,22 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="virtualScrolling">
+          <BooleanSetting path="virtualScrolling" expert="1">
             {{ $t('settings.virtual_scrolling') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="autohideFloatingPostButton">
+          <BooleanSetting path="alwaysShowNewPostButton" expert="1">
+            {{ $t('settings.always_show_post_button') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="autohideFloatingPostButton" expert="1">
             {{ $t('settings.autohide_floating_post_button') }}
           </BooleanSetting>
         </li>
         <li v-if="instanceShoutboxPresent">
-          <BooleanSetting path="hideShoutbox">
+          <BooleanSetting path="hideShoutbox" expert="1">
             {{ $t('settings.hide_shoutbox') }}
           </BooleanSetting>
         </li>
@@ -79,13 +85,18 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="emojiReactionsOnTimeline">
+          <BooleanSetting path="emojiReactionsOnTimeline" expert="1">
             {{ $t('settings.emoji_reactions_on_timeline') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="serverSide_stripRichContent" expert="1" v-if="user">
+            {{ $t('settings.no_rich_text_description') }}
+          </BooleanSetting>
+        </li>
         <h3>{{ $t('settings.attachments') }}</h3>
         <li>
-          <BooleanSetting path="useContainFit">
+          <BooleanSetting path="useContainFit" expert="1">
             {{ $t('settings.use_contain_fit') }}
           </BooleanSetting>
         </li>
@@ -97,7 +108,7 @@
         <ul class="setting-list suboptions">
           <li>
             <BooleanSetting
-              path="preloadImage"
+              path="preloadImage" expert="1"
               :disabled="!hideNsfw"
             >
               {{ $t('settings.preload_images') }}
@@ -105,7 +116,7 @@
           </li>
           <li>
             <BooleanSetting
-              path="useOneClickNsfw"
+              path="useOneClickNsfw" expert="1"
               :disabled="!hideNsfw"
             >
               {{ $t('settings.use_one_click_nsfw') }}
@@ -113,7 +124,7 @@
           </li>
         </ul>
         <li>
-          <BooleanSetting path="loopVideo">
+          <BooleanSetting path="loopVideo" expert="1">
             {{ $t('settings.loop_video') }}
           </BooleanSetting>
           <ul
@@ -122,7 +133,7 @@
           >
             <li>
               <BooleanSetting
-                path="loopVideoSilentOnly"
+                path="loopVideoSilentOnly" expert="1"
                 :disabled="!loopVideo || !loopSilentAvailable"
               >
                 {{ $t('settings.loop_video_silent_only') }}
@@ -137,21 +148,11 @@
           </ul>
         </li>
         <li>
-          <BooleanSetting path="playVideosInModal">
+          <BooleanSetting path="playVideosInModal" expert="1">
             {{ $t('settings.play_videos_in_modal') }}
           </BooleanSetting>
         </li>
-        <h3>{{ $t('settings.fun') }}</h3>
-        <li>
-          <BooleanSetting path="greentext">
-            {{ $t('settings.greentext') }}
-          </BooleanSetting>
-        </li>
-        <li>
-          <BooleanSetting path="mentionLinkShowYous">
-            {{ $t('settings.show_yous') }}
-          </BooleanSetting>
-        </li>
+        <h3>{{ $t('settings.mention_links') }}</h3>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
@@ -164,15 +165,14 @@
         <ul
           class="setting-list suboptions"
         >
-          <li
-            v-if="mentionLinkDisplay === 'short'"
-          >
-            <BooleanSetting path="mentionLinkShowTooltip">
+          <li v-if="mentionLinkDisplay === 'short'">
+            <BooleanSetting path="mentionLinkShowTooltip" expert="1">
               {{ $t('settings.mention_link_show_tooltip') }}
             </BooleanSetting>
           </li>
+        </ul>
           <li>
-            <BooleanSetting path="useAtIcon">
+            <BooleanSetting path="useAtIcon" expert="1">
               {{ $t('settings.use_at_icon') }}
             </BooleanSetting>
           </li>
@@ -182,29 +182,56 @@
             </BooleanSetting>
           </li>
           <li>
-            <BooleanSetting path="mentionLinkFadeDomain">
+            <BooleanSetting path="mentionLinkFadeDomain" expert="1">
               {{ $t('settings.mention_link_fade_domain') }}
             </BooleanSetting>
           </li>
-          <li>
-            <BooleanSetting path="mentionLinkBoldenYou">
+          <li v-if="user">
+            <BooleanSetting path="mentionLinkBoldenYou" expert="1">
               {{ $t('settings.mention_link_bolden_you') }}
             </BooleanSetting>
           </li>
-        </ul>
+        <h3 v-if="expertLevel > 0">{{ $t('settings.fun') }}</h3>
+        <li>
+          <BooleanSetting path="greentext" expert="1">
+            {{ $t('settings.greentext') }}
+          </BooleanSetting>
+        </li>
+        <li v-if="user">
+          <BooleanSetting path="mentionLinkShowYous" expert="1">
+            {{ $t('settings.show_yous') }}
+          </BooleanSetting>
+        </li>
       </ul>
     </div>
 
-    <div class="setting-item">
+    <div class="setting-item" v-if="user">
       <h2>{{ $t('settings.composing') }}</h2>
       <ul class="setting-list">
         <li>
-          <BooleanSetting path="scopeCopy">
+          <label for="default-vis">
+            {{ $t('settings.default_vis') }} <ServerSideIndicator :serverSide="true"/>
+            <ScopeSelector
+              class="scope-selector"
+              :show-all="true"
+              :user-default="serverSide_defaultScope"
+              :initial-scope="serverSide_defaultScope"
+              :on-scope-change="changeDefaultScope"
+            />
+          </label>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_defaultNSFW">
+            {{ $t('settings.sensitive_by_default') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="scopeCopy" expert="1">
             {{ $t('settings.scope_copy') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="alwaysShowSubjectInput">
+          <BooleanSetting path="alwaysShowSubjectInput" expert="1">
             {{ $t('settings.subject_input_always_show') }}
           </BooleanSetting>
         </li>
@@ -213,6 +240,7 @@
             id="subjectLineBehavior"
             path="subjectLineBehavior"
             :options="subjectLineOptions"
+            expert="1"
           >
             {{ $t('settings.subject_line_behavior') }}
           </ChoiceSetting>
@@ -227,43 +255,27 @@
           </ChoiceSetting>
         </li>
         <li>
-          <BooleanSetting path="minimalScopesMode">
+          <BooleanSetting path="minimalScopesMode" expert="1">
             {{ $t('settings.minimal_scopes_mode') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="sensitiveByDefault">
-            {{ $t('settings.sensitive_by_default') }}
-          </BooleanSetting>
-        </li>
-        <li>
-          <BooleanSetting path="alwaysShowNewPostButton">
+          <BooleanSetting path="alwaysShowNewPostButton" expert="1">
             {{ $t('settings.always_show_post_button') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="autohideFloatingPostButton">
+          <BooleanSetting path="autohideFloatingPostButton" expert="1">
             {{ $t('settings.autohide_floating_post_button') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="padEmoji">
+          <BooleanSetting path="padEmoji" expert="1">
             {{ $t('settings.pad_emoji') }}
           </BooleanSetting>
         </li>
       </ul>
     </div>
-
-    <div class="setting-item">
-      <h2>{{ $t('settings.notifications') }}</h2>
-      <ul class="setting-list">
-        <li>
-          <BooleanSetting path="webPushNotifications">
-            {{ $t('settings.enable_web_push_notifications') }}
-          </BooleanSetting>
-        </li>
-      </ul>
-    </div>
   </div>
 </template>
 
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
index 3e44c95d..3c6ab87f 100644
--- a/src/components/settings_modal/tabs/notifications_tab.js
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -1,4 +1,5 @@
-import Checkbox from 'src/components/checkbox/checkbox.vue'
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
 
 const NotificationsTab = {
   data () {
@@ -9,12 +10,13 @@ const NotificationsTab = {
     }
   },
   components: {
-    Checkbox
+    BooleanSetting
   },
   computed: {
     user () {
       return this.$store.state.users.currentUser
-    }
+    },
+    ...SharedComputedObject()
   },
   methods: {
     updateNotificationSettings () {
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 7e0568ea..5e9ce91e 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -2,30 +2,68 @@
   <div :label="$t('settings.notifications')">
     <div class="setting-item">
       <h2>{{ $t('settings.notification_setting_filters') }}</h2>
-      <p>
-        <Checkbox v-model="notificationSettings.block_from_strangers">
-          {{ $t('settings.notification_setting_block_from_strangers') }}
-        </Checkbox>
-      </p>
+      <ul class="setting-list">
+        <li>
+          <BooleanSetting path="serverSide_blockNotificationsFromStrangers">
+            {{ $t('settings.notification_setting_block_from_strangers') }}
+          </BooleanSetting>
+        </li>
+        <li class="select-multiple">
+          <span class="label">{{ $t('settings.notification_visibility') }}</span>
+          <ul class="option-list">
+            <li>
+              <BooleanSetting path="notificationVisibility.likes">
+                {{ $t('settings.notification_visibility_likes') }}
+              </BooleanSetting>
+            </li>
+            <li>
+              <BooleanSetting path="notificationVisibility.repeats">
+                {{ $t('settings.notification_visibility_repeats') }}
+              </BooleanSetting>
+            </li>
+            <li>
+              <BooleanSetting path="notificationVisibility.follows">
+                {{ $t('settings.notification_visibility_follows') }}
+              </BooleanSetting>
+            </li>
+            <li>
+              <BooleanSetting path="notificationVisibility.mentions">
+                {{ $t('settings.notification_visibility_mentions') }}
+              </BooleanSetting>
+            </li>
+            <li>
+              <BooleanSetting path="notificationVisibility.moves">
+                {{ $t('settings.notification_visibility_moves') }}
+              </BooleanSetting>
+            </li>
+            <li>
+              <BooleanSetting path="notificationVisibility.emojiReactions">
+                {{ $t('settings.notification_visibility_emoji_reactions') }}
+              </BooleanSetting>
+            </li>
+          </ul>
+        </li>
+      </ul>
     </div>
 
-    <div class="setting-item">
+    <div class="setting-item" v-if="expertLevel > 0">
       <h2>{{ $t('settings.notification_setting_privacy') }}</h2>
-      <p>
-        <Checkbox v-model="notificationSettings.hide_notification_contents">
-          {{ $t('settings.notification_setting_hide_notification_contents') }}
-        </Checkbox>
-      </p>
+      <ul class="setting-list">
+        <li>
+          <BooleanSetting path="webPushNotifications" expert="1">
+            {{ $t('settings.enable_web_push_notifications') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_webPushHideContents" expert="1">
+            {{ $t('settings.notification_setting_hide_notification_contents') }}
+          </BooleanSetting>
+        </li>
+      </ul>
     </div>
     <div class="setting-item">
       <p>{{ $t('settings.notification_mutes') }}</p>
       <p>{{ $t('settings.notification_blocks') }}</p>
-      <button
-        class="btn button-default"
-        @click="updateNotificationSettings"
-      >
-        {{ $t('settings.save') }}
-      </button>
     </div>
   </div>
 </template>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 64079fcd..bee8a7bb 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -8,6 +8,9 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
 import suggestor from 'src/components/emoji_input/suggestor.js'
 import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faTimes,
@@ -27,18 +30,10 @@ const ProfileTab = {
       newName: this.$store.state.users.currentUser.name_unescaped,
       newBio: unescape(this.$store.state.users.currentUser.description),
       newLocked: this.$store.state.users.currentUser.locked,
-      newNoRichText: this.$store.state.users.currentUser.no_rich_text,
-      newDefaultScope: this.$store.state.users.currentUser.default_scope,
       newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
-      hideFollows: this.$store.state.users.currentUser.hide_follows,
-      hideFollowers: this.$store.state.users.currentUser.hide_followers,
-      hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
-      hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
       showRole: this.$store.state.users.currentUser.show_role,
       role: this.$store.state.users.currentUser.role,
-      discoverable: this.$store.state.users.currentUser.discoverable,
       bot: this.$store.state.users.currentUser.bot,
-      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
       pickAvatarBtnVisible: true,
       bannerUploading: false,
       backgroundUploading: false,
@@ -54,12 +49,14 @@ const ProfileTab = {
     EmojiInput,
     Autosuggest,
     ProgressButton,
-    Checkbox
+    Checkbox,
+    BooleanSetting
   },
   computed: {
     user () {
       return this.$store.state.users.currentUser
     },
+    ...SharedComputedObject(),
     emojiUserSuggestor () {
       return suggestor({
         emoji: [
@@ -123,15 +120,7 @@ const ProfileTab = {
             /* eslint-disable camelcase */
             display_name: this.newName,
             fields_attributes: this.newFields.filter(el => el != null),
-            default_scope: this.newDefaultScope,
-            no_rich_text: this.newNoRichText,
-            hide_follows: this.hideFollows,
-            hide_followers: this.hideFollowers,
-            discoverable: this.discoverable,
             bot: this.bot,
-            allow_following_move: this.allowFollowingMove,
-            hide_follows_count: this.hideFollowsCount,
-            hide_followers_count: this.hideFollowersCount,
             show_role: this.showRole
             /* eslint-enable camelcase */
           } }).then((user) => {
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index bb3c301d..2cf3e8be 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -25,61 +25,6 @@
           class="bio resize-height"
         />
       </EmojiInput>
-      <p>
-        <Checkbox v-model="newLocked">
-          {{ $t('settings.lock_account_description') }}
-        </Checkbox>
-      </p>
-      <div>
-        <label for="default-vis">{{ $t('settings.default_vis') }}</label>
-        <div
-          id="default-vis"
-          class="visibility-tray"
-        >
-          <scope-selector
-            :show-all="true"
-            :user-default="newDefaultScope"
-            :initial-scope="newDefaultScope"
-            :on-scope-change="changeVis"
-          />
-        </div>
-      </div>
-      <p>
-        <Checkbox v-model="newNoRichText">
-          {{ $t('settings.no_rich_text_description') }}
-        </Checkbox>
-      </p>
-      <p>
-        <Checkbox v-model="hideFollows">
-          {{ $t('settings.hide_follows_description') }}
-        </Checkbox>
-      </p>
-      <p class="setting-subitem">
-        <Checkbox
-          v-model="hideFollowsCount"
-          :disabled="!hideFollows"
-        >
-          {{ $t('settings.hide_follows_count_description') }}
-        </Checkbox>
-      </p>
-      <p>
-        <Checkbox v-model="hideFollowers">
-          {{ $t('settings.hide_followers_description') }}
-        </Checkbox>
-      </p>
-      <p class="setting-subitem">
-        <Checkbox
-          v-model="hideFollowersCount"
-          :disabled="!hideFollowers"
-        >
-          {{ $t('settings.hide_followers_count_description') }}
-        </Checkbox>
-      </p>
-      <p>
-        <Checkbox v-model="allowFollowingMove">
-          {{ $t('settings.allow_following_move') }}
-        </Checkbox>
-      </p>
       <p v-if="role === 'admin' || role === 'moderator'">
         <Checkbox v-model="showRole">
           <template v-if="role === 'admin'">
@@ -90,11 +35,6 @@
           </template>
         </Checkbox>
       </p>
-      <p>
-        <Checkbox v-model="discoverable">
-          {{ $t('settings.discoverable') }}
-        </Checkbox>
-      </p>
       <div v-if="maxFields > 0">
         <p>{{ $t('settings.profile_fields.label') }}</p>
         <div
@@ -161,8 +101,7 @@
       <p class="visibility-notice">
         {{ $t('settings.avatar_size_instruction') }}
       </p>
-      <div class="current-avatar-container">
-        <img
+      <div class="current-avatar-container"> <img
           :src="user.profile_image_url_original"
           class="current-avatar"
         >
@@ -269,6 +208,67 @@
         {{ $t('settings.save') }}
       </button>
     </div>
+    <div class="setting-item">
+      <h2>{{ $t('settings.account_privacy') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <BooleanSetting path="serverSide_locked">
+            {{ $t('settings.lock_account_description') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_discoverable">
+            {{ $t('settings.discoverable') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_allowFollowingMove">
+            {{ $t('settings.allow_following_move') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_hideFavorites">
+            {{ $t('settings.hide_favorites_description') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_hideFollowers">
+            {{ $t('settings.hide_followers_description') }}
+          </BooleanSetting>
+          <ul
+            class="setting-list suboptions"
+            :class="[{disabled: !serverSide_hideFollowers}]"
+          >
+            <li>
+              <BooleanSetting
+                path="serverSide_hideFollowersCount"
+                :disabled="!severSide_hideFollowers"
+                >
+                {{ $t('settings.hide_followers_count_description') }}
+              </BooleanSetting>
+            </li>
+          </ul>
+        </li>
+        <li>
+          <BooleanSetting path="serverSide_hideFollows">
+            {{ $t('settings.hide_follows_description') }}
+          </BooleanSetting>
+          <ul
+            class="setting-list suboptions"
+            :class="[{disabled: !serverSide_hideFollows}]"
+          >
+            <li>
+              <BooleanSetting
+                path="serverSide_hideFollowsCount"
+                :disabled="!severSide_hideFollows"
+                >
+                {{ $t('settings.hide_follows_count_description') }}
+              </BooleanSetting>
+            </li>
+          </ul>
+        </li>
+      </ul>
+    </div>
   </div>
 </template>
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 8eb7fcc6..932e8af5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -259,11 +259,14 @@
   },
   "settings": {
     "app_name": "App name",
+    "expert_mode": "Expert mode",
     "save": "Save changes",
     "security": "Security",
     "setting_changed": "Setting is different from default",
+    "setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
     "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
     "post_look_feel": "Posts Look & Feel",
+    "mention_links": "Mention links",
     "mfa": {
       "otp": "OTP",
       "setup_otp": "Setup OTP",
@@ -400,6 +403,7 @@
       "name": "Label",
       "value": "Content"
     },
+    "account_privacy": "Privacy",
     "use_contain_fit": "Don't crop the attachment in thumbnails",
     "name": "Name",
     "name_bio": "Name & bio",
@@ -417,6 +421,7 @@
     "no_rich_text_description": "Strip rich text formatting from all posts",
     "no_blocks": "No blocks",
     "no_mutes": "No mutes",
+    "hide_favorites_description": "Don't show list of my favorites (people still get notified)",
     "hide_follows_description": "Don't show who I'm following",
     "hide_followers_description": "Don't show who's following me",
     "hide_follows_count_description": "Don't show follow count",
diff --git a/src/main.js b/src/main.js
index 3895da89..73bd49ed 100644
--- a/src/main.js
+++ b/src/main.js
@@ -11,6 +11,7 @@ import statusesModule from './modules/statuses.js'
 import usersModule from './modules/users.js'
 import apiModule from './modules/api.js'
 import configModule from './modules/config.js'
+import serverSideConfigModule from './modules/serverSideConfig.js'
 import shoutModule from './modules/shout.js'
 import oauthModule from './modules/oauth.js'
 import authFlowModule from './modules/auth_flow.js'
@@ -88,6 +89,7 @@ const persistedStateOptions = {
       users: usersModule,
       api: apiModule,
       config: configModule,
+      serverSideConfig: serverSideConfigModule,
       shout: shoutModule,
       oauth: oauthModule,
       authFlow: authFlowModule,
diff --git a/src/modules/config.js b/src/modules/config.js
index 20979174..775ee398 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -16,6 +16,7 @@ export const multiChoiceProperties = [
 ]
 
 export const defaultState = {
+  expertLevel: 0, // used to track which settings to show and hide
   colors: {},
   theme: undefined,
   customTheme: undefined,
diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js
new file mode 100644
index 00000000..75ea91be
--- /dev/null
+++ b/src/modules/serverSideConfig.js
@@ -0,0 +1,111 @@
+import { get, set } from 'lodash'
+
+export const settingsMapGet = {
+  'defaultScope': 'source.privacy',
+  'defaultNSFW': 'source.sensitive',
+  'stripRichContent': 'source.pleroma.no_rich_content',
+  // Privacy
+  'locked': 'locked',
+  'acceptChatMessages': 'pleroma.accepts_chat_messages',
+  'allowFollowingMove': 'pleroma.allow_following_move',
+  'discoverable': 'source.discoverable',
+  'hideFavorites': 'pleroma.hide_favorites',
+  'hideFollowers': 'pleroma.hide_followers',
+  'hideFollows': 'pleroma.hide_follows',
+  'hideFollowersCount': 'pleroma.hide_followers_count',
+  'hideFollowsCount': 'pleroma.hide_follows_count',
+  // NotificationSettingsAPIs
+  'webPushHideContents': 'pleroma.notification_settings.hide_notification_contents',
+  'blockNotificationsFromStrangers': 'pleroma.notification_settings.block_from_strangers'
+}
+
+export const settingsMapSet = {
+  'defaultScope': 'source.privacy',
+  'defaultNSFW': 'source.sensitive',
+  'stripRichContent': 'source.pleroma.no_rich_content',
+  // Privacy
+  'locked': 'locked',
+  'acceptChatMessages': 'accepts_chat_messages',
+  'allowFollowingMove': 'allow_following_move',
+  'discoverable': 'source.discoverable',
+  'hideFavorites': 'hide_favorites',
+  'hideFollowers': 'hide_followers',
+  'hideFollows': 'hide_follows',
+  'hideFollowersCount': 'hide_followers_count',
+  'hideFollowsCount': 'hide_follows_count',
+  // NotificationSettingsAPIs
+  'webPushHideContents': 'hide_notification_contents',
+  'blockNotificationsFromStrangers': 'block_from_strangers'
+}
+
+export const customAPIs = {
+  __defaultApi: 'updateProfile',
+  'webPushHideContents': 'updateNotificationSettings',
+  'blockNotificationsFromStrangers': 'updateNotificationSettings'
+}
+
+export const defaultState = Object.fromEntries(Object.keys(settingsMapGet).map(key => [key, undefined]))
+
+const serverSideConfig = {
+  state: { ...defaultState },
+  mutations: {
+    confirmServerSideOption (state, { name, value }) {
+      set(state, name, value)
+    },
+    wipeServerSideOption (state, { name }) {
+      set(state, name, undefined)
+    },
+    // Set the settings based on their path location
+    setCurrentUser (state, user) {
+      Object.entries(settingsMapGet).forEach(([name, path]) => {
+        set(state, name, get(user._original, path))
+      })
+    }
+  },
+  actions: {
+    setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) {
+      const oldValue = get(state, name)
+      const params = {}
+      const path = settingsMapSet[name]
+      if (!path) throw new Error('Invalid server-side setting')
+      commit('wipeServerSideOption', { name })
+      const customAPIName = customAPIs[name] || customAPIs.__defaultApi
+      const api = rootState.api.backendInteractor[customAPIName]
+      let prefix = ''
+      switch (customAPIName) {
+        case 'updateNotificationSettings':
+          prefix = 'settings.'
+          break
+        default:
+          prefix = 'params.'
+          break
+      }
+
+      set(params, prefix + path, value)
+      api(params)
+        .then((result) => {
+          switch (customAPIName) {
+            case 'updateNotificationSettings':
+              console.log(result)
+              if (result.status === 'success') {
+                commit('confirmServerSideOption', { name, value })
+              } else {
+                commit('confirmServerSideOption', { name, value: oldValue })
+              }
+              break
+            default:
+              commit('addNewUsers', [result])
+              commit('setCurrentUser', result)
+              break
+          }
+          console.log(state)
+        })
+        .catch((e) => {
+          console.warn('Error setting server-side option:', e)
+          commit('confirmServerSideOption', { name, value: oldValue })
+        })
+    }
+  }
+}
+
+export default serverSideConfig
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 7025d803..f219c161 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -44,6 +44,7 @@ export const parseUser = (data) => {
   const mastoShort = masto && !data.hasOwnProperty('avatar')
 
   output.id = String(data.id)
+  output._original = data // used for server-side settings
 
   if (masto) {
     output.screen_name = data.acct
-- 
cgit v1.2.3-70-g09d2


From f626da838a7abeb3a2d3cd5f71ee4eb2ca272361 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 24 Feb 2022 15:00:08 +0200
Subject: revert to using local setting for default nsfw since backend is
 broken

---
 src/components/post_status_form/post_status_form.js | 1 +
 src/components/settings_modal/tabs/general_tab.vue  | 3 ++-
 src/modules/serverSideConfig.js                     | 2 +-
 3 files changed, 4 insertions(+), 2 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index fe07309f..9d7bbd75 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -117,6 +117,7 @@ const PostStatusForm = {
       ? this.copyMessageScope
       : this.$store.state.users.currentUser.default_scope
 
+    // const { defaultNSFW: sensitiveByDefault } = this.$store.state.serverSideConfig
     const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
 
     return {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 4accf0c1..5db70d77 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -221,7 +221,8 @@
           </label>
         </li>
         <li>
-          <BooleanSetting path="serverSide_defaultNSFW">
+          <!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
+          <BooleanSetting path="sensitiveByDefault">
             {{ $t('settings.sensitive_by_default') }}
           </BooleanSetting>
         </li>
diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js
index 75ea91be..ea2dc5e3 100644
--- a/src/modules/serverSideConfig.js
+++ b/src/modules/serverSideConfig.js
@@ -2,7 +2,7 @@ import { get, set } from 'lodash'
 
 export const settingsMapGet = {
   'defaultScope': 'source.privacy',
-  'defaultNSFW': 'source.sensitive',
+  'defaultNSFW': 'source.sensitive', // BROKEN: pleroma/pleroma#2837
   'stripRichContent': 'source.pleroma.no_rich_content',
   // Privacy
   'locked': 'locked',
-- 
cgit v1.2.3-70-g09d2


From e3d602fdccb0dfe211fd3950210cdf8fda43bf95 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 28 Feb 2022 17:45:07 +0200
Subject: revert changes related to streaming/firehose setting, reword it so
 it's not confused with websocket streaming

---
 src/components/settings_modal/tabs/general_tab.vue | 3 +--
 src/i18n/en.json                                   | 4 ++--
 src/modules/config.js                              | 2 +-
 3 files changed, 4 insertions(+), 5 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 5db70d77..7944b03c 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -27,7 +27,7 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="streaming" expert="1">
+          <BooleanSetting path="streaming" >
             {{ $t('settings.streaming') }}
           </BooleanSetting>
           <ul
@@ -38,7 +38,6 @@
               <BooleanSetting
                 path="pauseOnUnfocused"
                 :disabled="!streaming"
-                expert="1"
               >
                 {{ $t('settings.pause_on_unfocused') }}
               </BooleanSetting>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a66aaa0b..da5e662d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -435,7 +435,7 @@
     "valid_until": "Valid until",
     "revoke_token": "Revoke",
     "panelRadius": "Panels",
-    "pause_on_unfocused": "Pause streaming when tab is not focused",
+    "pause_on_unfocused": "Pause when tab is not focused",
     "presets": "Presets",
     "profile_background": "Profile background",
     "profile_banner": "Profile banner",
@@ -473,7 +473,7 @@
     "post_status_content_type": "Post status content type",
     "sensitive_by_default": "Mark posts as sensitive by default",
     "stop_gifs": "Pause animated images until you hover on them",
-    "streaming": "Enable automatic streaming of new posts when scrolled to the top",
+    "streaming": "Automatically show new posts when scrolled to the top",
     "user_mutes": "Users",
     "useStreamingApi": "Receive posts and notifications real-time",
     "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
diff --git a/src/modules/config.js b/src/modules/config.js
index e5321507..86601564 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -37,7 +37,7 @@ export const defaultState = {
   preloadImage: true,
   loopVideo: true,
   loopVideoSilentOnly: true,
-  streaming: true,
+  streaming: false,
   emojiReactionsOnTimeline: true,
   alwaysShowNewPostButton: false,
   autohideFloatingPostButton: false,
-- 
cgit v1.2.3-70-g09d2


From 77bb0b553062b91a69fbf92ce7540565f098951f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 28 Feb 2022 18:23:32 +0200
Subject: lint

---
 src/components/attachment/attachment.vue           |  19 ++-
 src/components/gallery/gallery.vue                 |   4 +-
 .../settings_modal/helpers/boolean_setting.vue     |   4 +-
 .../settings_modal/helpers/choice_setting.vue      |   4 +-
 src/components/settings_modal/settings_modal.vue   |   4 +-
 .../settings_modal/tabs/filtering_tab.vue          |  16 +-
 src/components/settings_modal/tabs/general_tab.js  |   2 +-
 src/components/settings_modal/tabs/general_tab.vue | 167 +++++++++++++++------
 .../settings_modal/tabs/notifications_tab.vue      |  15 +-
 src/components/settings_modal/tabs/profile_tab.vue |   7 +-
 10 files changed, 168 insertions(+), 74 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 59173759..c4399f30 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -81,56 +81,56 @@
         <button
           v-if="type === 'flash' && flashLoaded"
           class="button-unstyled attachment-button"
-          @click.prevent="stopFlash"
           :title="$t('status.attachment_stop_flash')"
+          @click.prevent="stopFlash"
         >
           <FAIcon icon="stop" />
         </button>
         <button
           v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
           class="button-unstyled attachment-button"
-          @click.prevent="toggleDescription"
           :title="$t('status.show_attachment_description')"
+          @click.prevent="toggleDescription"
         >
           <FAIcon icon="align-right" />
         </button>
         <button
           v-if="!useModal && type !== 'unknown'"
           class="button-unstyled attachment-button"
-          @click.prevent="openModalForce"
           :title="$t('status.show_attachment_in_modal')"
+          @click.prevent="openModalForce"
         >
           <FAIcon icon="search-plus" />
         </button>
         <button
           v-if="nsfw && hideNsfwLocal"
           class="button-unstyled attachment-button"
-          @click.prevent="toggleHidden"
           :title="$t('status.hide_attachment')"
+          @click.prevent="toggleHidden"
         >
           <FAIcon icon="times" />
         </button>
         <button
           v-if="shiftUp"
           class="button-unstyled attachment-button"
-          @click.prevent="onShiftUp"
           :title="$t('status.move_up')"
+          @click.prevent="onShiftUp"
         >
           <FAIcon icon="chevron-left" />
         </button>
         <button
           v-if="shiftDn"
           class="button-unstyled attachment-button"
-          @click.prevent="onShiftDn"
           :title="$t('status.move_down')"
+          @click.prevent="onShiftDn"
         >
           <FAIcon icon="chevron-right" />
         </button>
         <button
           v-if="remove"
           class="button-unstyled attachment-button"
-          @click.prevent="onRemove"
           :title="$t('status.remove_attachment')"
+          @click.prevent="onRemove"
         >
           <FAIcon icon="trash-alt" />
         </button>
@@ -160,7 +160,10 @@
         :href="attachment.url"
         target="_blank"
       >
-        <FAIcon size="5x" :icon="placeholderIconClass" />
+        <FAIcon
+          size="5x"
+          :icon="placeholderIconClass"
+        />
         <p>
           {{ localDescription }}
         </p>
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index f9cad8a9..f2e1b5ce 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -26,8 +26,8 @@
             :size="size"
             :editable="editable"
             :remove="removeAttachment"
-            :shiftUp="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
-            :shiftDn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
+            :shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
+            :shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
             :edit="editAttachment"
             :description="descriptions && descriptions[attachment.id]"
             :hide-description="size === 'small' || tooManyAttachments && hidingLong"
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index 3a7bd805..e0d825f2 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -1,7 +1,7 @@
 <template>
   <label
-    class="BooleanSetting"
     v-if="matchesExpertLevel"
+    class="BooleanSetting"
   >
     <Checkbox
       :checked="state"
@@ -14,7 +14,7 @@
       >
         <slot />
       </span>
-      <ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :serverSide="isServerSide" /> </Checkbox>
+      <ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox>
   </label>
 </template>
 
diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue
index 845886ca..54f5d0a7 100644
--- a/src/components/settings_modal/helpers/choice_setting.vue
+++ b/src/components/settings_modal/helpers/choice_setting.vue
@@ -1,7 +1,7 @@
 <template>
   <label
-    class="ChoiceSetting"
     v-if="matchesExpertLevel"
+    class="ChoiceSetting"
   >
     <slot />
     <Select
@@ -19,7 +19,7 @@
       </option>
     </Select>
     <ModifiedIndicator :changed="isChanged" />
-    <ServerSideIndicator :serverSide="isServerSide" />
+    <ServerSideIndicator :server-side="isServerSide" />
   </label>
 </template>
 
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 0aad1abb..1805c77f 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -109,8 +109,8 @@
           </template>
         </Popover>
 
-        <Checkbox v-model="expertLevel" >
-          {{ $t("settings.expert_mode")}}
+        <Checkbox v-model="expertLevel">
+          {{ $t("settings.expert_mode") }}
         </Checkbox>
       </div>
     </div>
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 0352e887..e60a8a85 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -21,8 +21,8 @@
             </li>
             <li>
               <BooleanSetting
-                :disabled="hideFilteredStatuses"
                 v-if="user"
+                :disabled="hideFilteredStatuses"
                 path="hideMutedThreads"
               >
                 {{ $t('settings.hide_muted_threads') }}
@@ -30,8 +30,8 @@
             </li>
             <li>
               <BooleanSetting
-                :disabled="hideFilteredStatuses"
                 v-if="user"
+                :disabled="hideFilteredStatuses"
                 path="hideMutedPosts"
               >
                 {{ $t('settings.hide_muted_posts') }}
@@ -40,15 +40,18 @@
           </ul>
         </li>
         <li>
-          <BooleanSetting path="hidePostStats" expert="1">
+          <BooleanSetting
+            path="hidePostStats"
+            expert="1"
+          >
             {{ $t('settings.hide_post_stats') }}
           </BooleanSetting>
         </li>
         <ChoiceSetting
+          v-if="user"
           id="replyVisibility"
           path="replyVisibility"
           :options="replyVisibilityOptions"
-          v-if="user"
         >
           {{ $t('settings.replies_in_timeline') }}
         </ChoiceSetting>
@@ -87,7 +90,10 @@
         </li>
       </ul>
     </div>
-    <div class="setting-item" v-if="expertLevel > 0">
+    <div
+      v-if="expertLevel > 0"
+      class="setting-item"
+    >
       <h2>{{ $t('settings.user_profiles') }}</h2>
       <ul class="setting-list">
         <li>
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 9e4e282f..fe7deb6e 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -41,7 +41,7 @@ const GeneralTab = {
     ChoiceSetting,
     InterfaceLanguageSwitcher,
     ScopeSelector,
-    ServerSideIndicator,
+    ServerSideIndicator
   },
   computed: {
     postFormats () {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 7944b03c..d4196c3d 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -27,7 +27,7 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="streaming" >
+          <BooleanSetting path="streaming">
             {{ $t('settings.streaming') }}
           </BooleanSetting>
           <ul
@@ -45,7 +45,10 @@
           </ul>
         </li>
         <li>
-          <BooleanSetting path="useStreamingApi" expert="1">
+          <BooleanSetting
+            path="useStreamingApi"
+            expert="1"
+          >
             {{ $t('settings.useStreamingApi') }}
             <br>
             <small>
@@ -54,22 +57,34 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="virtualScrolling" expert="1">
+          <BooleanSetting
+            path="virtualScrolling"
+            expert="1"
+          >
             {{ $t('settings.virtual_scrolling') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="alwaysShowNewPostButton" expert="1">
+          <BooleanSetting
+            path="alwaysShowNewPostButton"
+            expert="1"
+          >
             {{ $t('settings.always_show_post_button') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="autohideFloatingPostButton" expert="1">
+          <BooleanSetting
+            path="autohideFloatingPostButton"
+            expert="1"
+          >
             {{ $t('settings.autohide_floating_post_button') }}
           </BooleanSetting>
         </li>
         <li v-if="instanceShoutboxPresent">
-          <BooleanSetting path="hideShoutbox" expert="1">
+          <BooleanSetting
+            path="hideShoutbox"
+            expert="1"
+          >
             {{ $t('settings.hide_shoutbox') }}
           </BooleanSetting>
         </li>
@@ -84,18 +99,28 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="emojiReactionsOnTimeline" expert="1">
+          <BooleanSetting
+            path="emojiReactionsOnTimeline"
+            expert="1"
+          >
             {{ $t('settings.emoji_reactions_on_timeline') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="serverSide_stripRichContent" expert="1" v-if="user">
+          <BooleanSetting
+            v-if="user"
+            path="serverSide_stripRichContent"
+            expert="1"
+          >
             {{ $t('settings.no_rich_text_description') }}
           </BooleanSetting>
         </li>
         <h3>{{ $t('settings.attachments') }}</h3>
         <li>
-          <BooleanSetting path="useContainFit" expert="1">
+          <BooleanSetting
+            path="useContainFit"
+            expert="1"
+          >
             {{ $t('settings.use_contain_fit') }}
           </BooleanSetting>
         </li>
@@ -107,7 +132,8 @@
         <ul class="setting-list suboptions">
           <li>
             <BooleanSetting
-              path="preloadImage" expert="1"
+              path="preloadImage"
+              expert="1"
               :disabled="!hideNsfw"
             >
               {{ $t('settings.preload_images') }}
@@ -115,7 +141,8 @@
           </li>
           <li>
             <BooleanSetting
-              path="useOneClickNsfw" expert="1"
+              path="useOneClickNsfw"
+              expert="1"
               :disabled="!hideNsfw"
             >
               {{ $t('settings.use_one_click_nsfw') }}
@@ -123,7 +150,10 @@
           </li>
         </ul>
         <li>
-          <BooleanSetting path="loopVideo" expert="1">
+          <BooleanSetting
+            path="loopVideo"
+            expert="1"
+          >
             {{ $t('settings.loop_video') }}
           </BooleanSetting>
           <ul
@@ -132,7 +162,8 @@
           >
             <li>
               <BooleanSetting
-                path="loopVideoSilentOnly" expert="1"
+                path="loopVideoSilentOnly"
+                expert="1"
                 :disabled="!loopVideo || !loopSilentAvailable"
               >
                 {{ $t('settings.loop_video_silent_only') }}
@@ -147,7 +178,10 @@
           </ul>
         </li>
         <li>
-          <BooleanSetting path="playVideosInModal" expert="1">
+          <BooleanSetting
+            path="playVideosInModal"
+            expert="1"
+          >
             {{ $t('settings.play_videos_in_modal') }}
           </BooleanSetting>
         </li>
@@ -165,51 +199,74 @@
           class="setting-list suboptions"
         >
           <li v-if="mentionLinkDisplay === 'short'">
-            <BooleanSetting path="mentionLinkShowTooltip" expert="1">
+            <BooleanSetting
+              path="mentionLinkShowTooltip"
+              expert="1"
+            >
               {{ $t('settings.mention_link_show_tooltip') }}
             </BooleanSetting>
           </li>
         </ul>
-          <li>
-            <BooleanSetting path="useAtIcon" expert="1">
-              {{ $t('settings.use_at_icon') }}
-            </BooleanSetting>
-          </li>
-          <li>
-            <BooleanSetting path="mentionLinkShowAvatar">
-              {{ $t('settings.mention_link_show_avatar') }}
-            </BooleanSetting>
-          </li>
-          <li>
-            <BooleanSetting path="mentionLinkFadeDomain" expert="1">
-              {{ $t('settings.mention_link_fade_domain') }}
-            </BooleanSetting>
-          </li>
-          <li v-if="user">
-            <BooleanSetting path="mentionLinkBoldenYou" expert="1">
-              {{ $t('settings.mention_link_bolden_you') }}
-            </BooleanSetting>
-          </li>
-        <h3 v-if="expertLevel > 0">{{ $t('settings.fun') }}</h3>
         <li>
-          <BooleanSetting path="greentext" expert="1">
+          <BooleanSetting
+            path="useAtIcon"
+            expert="1"
+          >
+            {{ $t('settings.use_at_icon') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting path="mentionLinkShowAvatar">
+            {{ $t('settings.mention_link_show_avatar') }}
+          </BooleanSetting>
+        </li>
+        <li>
+          <BooleanSetting
+            path="mentionLinkFadeDomain"
+            expert="1"
+          >
+            {{ $t('settings.mention_link_fade_domain') }}
+          </BooleanSetting>
+        </li>
+        <li v-if="user">
+          <BooleanSetting
+            path="mentionLinkBoldenYou"
+            expert="1"
+          >
+            {{ $t('settings.mention_link_bolden_you') }}
+          </BooleanSetting>
+        </li>
+        <h3 v-if="expertLevel > 0">
+          {{ $t('settings.fun') }}
+        </h3>
+        <li>
+          <BooleanSetting
+            path="greentext"
+            expert="1"
+          >
             {{ $t('settings.greentext') }}
           </BooleanSetting>
         </li>
         <li v-if="user">
-          <BooleanSetting path="mentionLinkShowYous" expert="1">
+          <BooleanSetting
+            path="mentionLinkShowYous"
+            expert="1"
+          >
             {{ $t('settings.show_yous') }}
           </BooleanSetting>
         </li>
       </ul>
     </div>
 
-    <div class="setting-item" v-if="user">
+    <div
+      v-if="user"
+      class="setting-item"
+    >
       <h2>{{ $t('settings.composing') }}</h2>
       <ul class="setting-list">
         <li>
           <label for="default-vis">
-            {{ $t('settings.default_vis') }} <ServerSideIndicator :serverSide="true"/>
+            {{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
             <ScopeSelector
               class="scope-selector"
               :show-all="true"
@@ -226,12 +283,18 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="scopeCopy" expert="1">
+          <BooleanSetting
+            path="scopeCopy"
+            expert="1"
+          >
             {{ $t('settings.scope_copy') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="alwaysShowSubjectInput" expert="1">
+          <BooleanSetting
+            path="alwaysShowSubjectInput"
+            expert="1"
+          >
             {{ $t('settings.subject_input_always_show') }}
           </BooleanSetting>
         </li>
@@ -255,22 +318,34 @@
           </ChoiceSetting>
         </li>
         <li>
-          <BooleanSetting path="minimalScopesMode" expert="1">
+          <BooleanSetting
+            path="minimalScopesMode"
+            expert="1"
+          >
             {{ $t('settings.minimal_scopes_mode') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="alwaysShowNewPostButton" expert="1">
+          <BooleanSetting
+            path="alwaysShowNewPostButton"
+            expert="1"
+          >
             {{ $t('settings.always_show_post_button') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="autohideFloatingPostButton" expert="1">
+          <BooleanSetting
+            path="autohideFloatingPostButton"
+            expert="1"
+          >
             {{ $t('settings.autohide_floating_post_button') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="padEmoji" expert="1">
+          <BooleanSetting
+            path="padEmoji"
+            expert="1"
+          >
             {{ $t('settings.pad_emoji') }}
           </BooleanSetting>
         </li>
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 5e9ce91e..86be6095 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -46,16 +46,25 @@
       </ul>
     </div>
 
-    <div class="setting-item" v-if="expertLevel > 0">
+    <div
+      v-if="expertLevel > 0"
+      class="setting-item"
+    >
       <h2>{{ $t('settings.notification_setting_privacy') }}</h2>
       <ul class="setting-list">
         <li>
-          <BooleanSetting path="webPushNotifications" expert="1">
+          <BooleanSetting
+            path="webPushNotifications"
+            expert="1"
+          >
             {{ $t('settings.enable_web_push_notifications') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="serverSide_webPushHideContents" expert="1">
+          <BooleanSetting
+            path="serverSide_webPushHideContents"
+            expert="1"
+          >
             {{ $t('settings.notification_setting_hide_notification_contents') }}
           </BooleanSetting>
         </li>
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 699fdcf4..e00f6e5b 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -101,7 +101,8 @@
       <p class="visibility-notice">
         {{ $t('settings.avatar_size_instruction') }}
       </p>
-      <div class="current-avatar-container"> <img
+      <div class="current-avatar-container">
+        <img
           :src="user.profile_image_url_original"
           class="current-avatar"
         >
@@ -243,7 +244,7 @@
               <BooleanSetting
                 path="serverSide_hideFollowersCount"
                 :disabled="!serverSide_hideFollowers"
-                >
+              >
                 {{ $t('settings.hide_followers_count_description') }}
               </BooleanSetting>
             </li>
@@ -261,7 +262,7 @@
               <BooleanSetting
                 path="serverSide_hideFollowsCount"
                 :disabled="!serverSide_hideFollows"
-                >
+              >
                 {{ $t('settings.hide_follows_count_description') }}
               </BooleanSetting>
             </li>
-- 
cgit v1.2.3-70-g09d2


From 0582f19e7c2c6f916b427d5ecfbbb571178ce841 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 6 Aug 2021 20:18:27 -0400
Subject: Add tree-style thread display

---
 src/components/conversation/conversation.js        | 60 +++++++++++++++++++++-
 src/components/conversation/conversation.vue       | 59 ++++++++++++++-------
 src/components/settings_modal/tabs/general_tab.js  |  5 ++
 src/components/settings_modal/tabs/general_tab.vue |  9 ++++
 src/components/thread_tree/thread_tree.js          | 52 +++++++++++++++++++
 src/components/thread_tree/thread_tree.vue         | 55 ++++++++++++++++++++
 src/modules/config.js                              |  4 +-
 src/modules/instance.js                            |  1 +
 8 files changed, 224 insertions(+), 21 deletions(-)
 create mode 100644 src/components/thread_tree/thread_tree.js
 create mode 100644 src/components/thread_tree/thread_tree.vue

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 069c0b40..e663ba15 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,5 +1,8 @@
 import { reduce, filter, findIndex, clone, get } from 'lodash'
 import Status from '../status/status.vue'
+import ThreadTree from '../thread_tree/thread_tree.vue'
+
+const debug = console.log
 
 const sortById = (a, b) => {
   const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@@ -53,6 +56,15 @@ const conversation = {
     }
   },
   computed: {
+    displayStyle () {
+      return this.$store.state.config.conversationDisplay
+    },
+    isTreeView () {
+      return this.displayStyle === 'tree'
+    },
+    isLinearView () {
+      return this.displayStyle === 'linear'
+    },
     hideStatus () {
       if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
         return this.virtualHidden && this.$refs.statusComponent[0].suspendable
@@ -90,6 +102,49 @@ const conversation = {
 
       return sortAndFilterConversation(conversation, this.status)
     },
+    threadTree () {
+      const reverseLookupTable = this.conversation.reduce((table, status, index) => {
+        table[status.id] = index
+        return table
+      }, {})
+
+      const threads = this.conversation.reduce((a, cur) => {
+        const id = cur.id
+        a.forest[id] = this.getReplies(id)
+          .map(s => s.id)
+          .sort((a, b) => reverseLookupTable[a] - reverseLookupTable[b])
+
+        a.topLevel = a.topLevel.filter(k => a.forest[id].contains(k))
+        return a
+      }, {
+        forest: {},
+        topLevel: this.conversation.map(s => s.id)
+      })
+
+      const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
+        if (processed[id]) {
+          return []
+        }
+
+        processed[id] = true
+        return [{
+          status: this.conversation[reverseLookupTable[id]],
+          id,
+          depth
+        }, walk(forest, forest[child], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
+      }).reduce((a, b) => a.concat(b), [])
+
+      const linearized = walk(threads.forest, threads.topLevel)
+
+      return linearized
+    },
+    topLevel () {
+      const topLevel = this.conversation.reduce((tl, cur) =>
+        tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
+      debug("toplevel =", topLevel)
+      debug("toplevel =", topLevel)
+      return topLevel
+    },
     replies () {
       let i = 1
       // eslint-disable-next-line camelcase
@@ -109,7 +164,7 @@ const conversation = {
       }, {})
     },
     isExpanded () {
-      return this.expanded || this.isPage
+      return !!(this.expanded || this.isPage)
     },
     hiddenStyle () {
       const height = (this.status && this.status.virtualHeight) || '120px'
@@ -117,7 +172,8 @@ const conversation = {
     }
   },
   components: {
-    Status
+    Status,
+    ThreadTree
   },
   watch: {
     statusId (newVal, oldVal) {
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3fb26d92..cea5f88f 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -18,24 +18,47 @@
         {{ $t('timeline.collapse') }}
       </button>
     </div>
-    <status
-      v-for="status in conversation"
-      :key="status.id"
-      ref="statusComponent"
-      :inline-expanded="collapsable && isExpanded"
-      :statusoid="status"
-      :expandable="!isExpanded"
-      :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
-      :focused="focused(status.id)"
-      :in-conversation="isExpanded"
-      :highlight="getHighlight()"
-      :replies="getReplies(status.id)"
-      :in-profile="inProfile"
-      :profile-user-id="profileUserId"
-      class="conversation-status status-fadein panel-body"
-      @goto="setHighlight"
-      @toggleExpanded="toggleExpanded"
-    />
+    <div v-if="isTreeView">
+      <thread-tree
+        v-for="status in topLevel"
+        :key="status.id"
+        ref="statusComponent"
+
+        :status="status"
+        :in-profile="inProfile"
+        :conversation="conversation"
+        :collapsable="collapsable"
+        :is-expanded="isExpanded"
+        :pinned-status-ids-object="pinnedStatusIdsObject"
+        :profile-user-id="profileUserId"
+
+        :focused="focused"
+        :get-replies="getReplies"
+        :get-highlight="getHighlight"
+        :set-highlight="setHighlight"
+        :toggle-expanded="toggleExpanded"
+      />
+    </div>
+    <div v-if="isLinearView">
+      <status
+        v-for="status in conversation"
+        :key="status.id"
+        ref="statusComponent"
+        :inline-expanded="collapsable && isExpanded"
+        :statusoid="status"
+        :expandable="!isExpanded"
+        :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
+        :focused="focused(status.id)"
+        :in-conversation="isExpanded"
+        :highlight="getHighlight()"
+        :replies="getReplies(status.id)"
+        :in-profile="inProfile"
+        :profile-user-id="profileUserId"
+        class="conversation-status status-fadein panel-body"
+        @goto="setHighlight"
+        @toggleExpanded="toggleExpanded"
+      />
+    </div>
   </div>
   <div
     v-else
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 952c328d..abe8f6f7 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -20,6 +20,11 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
       })),
+      conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.conversation_display_${mode}`)
+      })),
       mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
         key: mode,
         value: mode,
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index eba3b268..8951c021 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -152,6 +152,15 @@
             {{ $t('settings.show_yous') }}
           </BooleanSetting>
         </li>
+        <li>
+          <ChoiceSetting
+            id="conversationDisplay"
+            path="conversationDisplay"
+            :options="conversationDisplayOptions"
+          >
+            {{ $t('settings.conversation_display') }}
+          </ChoiceSetting>
+        </li>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
new file mode 100644
index 00000000..4f95122a
--- /dev/null
+++ b/src/components/thread_tree/thread_tree.js
@@ -0,0 +1,52 @@
+import Status from '../status/status.vue'
+
+const debug = console.log
+
+const ThreadTree = {
+  components: {
+    Status
+  },
+  name: 'ThreadTree',
+  props: {
+    depth: Number,
+    status: Object,
+    inProfile: Boolean,
+    conversation: Array,
+    collapsable: Boolean,
+    isExpanded: Boolean,
+    pinnedStatusIdsObject: Object,
+    profileUserId: String,
+
+    focused: Function,
+    getHighlight: Function,
+    getReplies: Function,
+    setHighlight: Function,
+    toggleExpanded: Function
+  },
+  computed: {
+    reverseLookupTable () {
+      return this.conversation.reduce((table, status, index) => {
+        table[status.id] = index
+        return table
+      }, {})
+    },
+    currentReplies () {
+      debug('status:', this.status)
+      debug('getReplies:', this.getReplies(this.status.id))
+      return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
+    },
+  },
+  methods: {
+    statusById (id) {
+      return this.conversation[this.reverseLookupTable[id]]
+    },
+    collapseThread () {
+    },
+    showThread () {
+    },
+    showAllSubthreads () {
+    }
+  }
+}
+
+export default ThreadTree
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
new file mode 100644
index 00000000..8256eee6
--- /dev/null
+++ b/src/components/thread_tree/thread_tree.vue
@@ -0,0 +1,55 @@
+<template>
+  <div class="thread-tree panel-body">
+    <status
+      :key="status.id"
+      ref="statusComponent"
+      :inline-expanded="collapsable && isExpanded"
+      :statusoid="status"
+      :expandable="!isExpanded"
+      :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
+      :focused="focused(status.id)"
+      :in-conversation="isExpanded"
+      :highlight="getHighlight()"
+      :replies="getReplies(status.id)"
+      :in-profile="inProfile"
+      :profile-user-id="profileUserId"
+      class="conversation-status status-fadein panel-body"
+      @goto="setHighlight"
+      @toggleExpanded="toggleExpanded"
+    />
+    <div
+      v-if="currentReplies.length"
+      class="thread-tree-replies"
+    >
+      <thread-tree
+        v-for="replyStatus in currentReplies"
+        :key="replyStatus.id"
+        ref="childComponent"
+        :status="replyStatus"
+
+        :in-profile="inProfile"
+        :conversation="conversation"
+        :collapsable="collapsable"
+        :is-expanded="isExpanded"
+        :pinned-status-ids-object="pinnedStatusIdsObject"
+        :profile-user-id="profileUserId"
+
+        :focused="focused"
+        :get-replies="getReplies"
+        :get-highlight="getHighlight"
+        :set-highlight="setHighlight"
+        :toggle-expanded="toggleExpanded"
+
+        class="conversation-status status-fadein panel-body"
+      />
+    </div>
+  </div>
+</template>
+
+<script src="./thread_tree.js"></script>
+
+<style lang="scss">
+.thread-tree-replies {
+  margin-left: 1em;
+}
+</style>
diff --git a/src/modules/config.js b/src/modules/config.js
index 20979174..ec75dbfb 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -12,6 +12,7 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
 export const multiChoiceProperties = [
   'postContentType',
   'subjectLineBehavior',
+  'conversationDisplay', // tree | linear
   'mentionLinkDisplay' // short | full_for_remote | full
 ]
 
@@ -81,7 +82,8 @@ export const defaultState = {
   hidePostStats: undefined, // instance default
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
-  sensitiveByDefault: undefined // instance default
+  sensitiveByDefault: undefined, // instance default
+  conversationDisplay: undefined // instance default
 }
 
 // caching the instance default properties
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 1abd784f..a160a557 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -53,6 +53,7 @@ const defaultState = {
   theme: 'pleroma-dark',
   virtualScrolling: true,
   sensitiveByDefault: false,
+  conversationDisplay: 'tree',
 
   // Nasty stuff
   customEmoji: [],
-- 
cgit v1.2.3-70-g09d2


From 2e54cf12c76e94f980fbe49f58c080a4a3d17ca4 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 7 Aug 2021 20:14:14 -0400
Subject: Add settings for max depth in thread

---
 src/components/conversation/conversation.js        | 15 ++++++++++-----
 src/components/settings_modal/tabs/general_tab.vue | 13 +++++++++++++
 src/modules/config.js                              |  3 ++-
 src/modules/instance.js                            |  1 +
 4 files changed, 26 insertions(+), 6 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 8e4eda51..853161df 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -74,7 +74,11 @@ const conversation = {
   },
   computed: {
     maxDepthToShowByDefault () {
-      return 4
+      // maxDepthInThread = max number of depths that is *visible*
+      // since our depth starts with 0 and "showing" means "showing children"
+      // there is a -2 here
+      const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
+      return maxDepth >= 1 ? maxDepth : 1
     },
     displayStyle () {
       return this.$store.getters.mergedConfig.conversationDisplay
@@ -351,7 +355,8 @@ const conversation = {
         }
         this._diven = true
         const parentOrSelf = this.parentOrSelf(this.originalStatusId)
-        if (this.threadDisplayStatus[this.statusId] === 'hidden') {
+        // If current status is not visible
+        if (this.threadDisplayStatus[parentOrSelf] === 'hidden') {
           this.diveIntoStatus(parentOrSelf, /* preventScroll */ true)
           this.tryScrollTo(this.statusId)
         }
@@ -429,7 +434,7 @@ const conversation = {
     toggleStatusContentProperty (id, name) {
       this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
     },
-    leastShowingAncestor (id) {
+    leastVisibleAncestor (id) {
       let cur = id
       let parent = this.parentOf(cur)
       while (cur) {
@@ -453,14 +458,14 @@ const conversation = {
       const oldHighlight = this.highlight
       this.diveHistory = [...this.diveHistory.slice(0, this.diveHistory.length - 1)]
       if (oldHighlight) {
-        this.tryScrollTo(this.leastShowingAncestor(oldHighlight))
+        this.tryScrollTo(this.leastVisibleAncestor(oldHighlight))
       }
     },
     undive () {
       const oldHighlight = this.highlight
       this.diveHistory = []
       if (oldHighlight) {
-        this.tryScrollTo(this.leastShowingAncestor(oldHighlight))
+        this.tryScrollTo(this.leastVisibleAncestor(oldHighlight))
       } else {
         this.goToCurrent()
       }
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 8951c021..f97d92c3 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -161,6 +161,19 @@
             {{ $t('settings.conversation_display') }}
           </ChoiceSetting>
         </li>
+        <li v-if="conversationDisplay !== 'linear'">
+          <label for="maxDepthInThread">
+            {{ $t('settings.max_depth_in_thread') }}
+          </label>
+          <input
+            id="maxDepthInThread"
+            path.number="maxDepthInThread"
+            class="number-input"
+            type="number"
+            min="3"
+            step="1"
+          >
+        </li>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
diff --git a/src/modules/config.js b/src/modules/config.js
index ec75dbfb..b3c7e5ab 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -83,7 +83,8 @@ export const defaultState = {
   hideUserStats: undefined, // instance default
   virtualScrolling: undefined, // instance default
   sensitiveByDefault: undefined, // instance default
-  conversationDisplay: undefined // instance default
+  conversationDisplay: undefined, // instance default
+  maxDepthInThread: 6
 }
 
 // caching the instance default properties
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 8a5bbbd1..853be737 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -54,6 +54,7 @@ const defaultState = {
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'simple_tree',
+  maxDepthInThread: 6,
 
   // Nasty stuff
   customEmoji: [],
-- 
cgit v1.2.3-70-g09d2


From 863255d52fdcbbabe45c86c7e36ebafc0f7e1c53 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 5 Sep 2021 16:35:47 -0400
Subject: Make position of other replies button a pref

---
 src/components/conversation/conversation.js        |  9 +++++
 src/components/conversation/conversation.vue       |  7 ++--
 src/components/settings_modal/tabs/general_tab.js  |  5 +++
 src/components/settings_modal/tabs/general_tab.vue | 40 +++++++++++++++-------
 src/components/status/status.js                    |  1 +
 src/components/status/status.vue                   | 15 +++++++-
 src/modules/config.js                              |  2 ++
 src/modules/instance.js                            |  1 +
 8 files changed, 63 insertions(+), 17 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 423930af..d4972fbc 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -92,6 +92,15 @@ const conversation = {
     isLinearView () {
       return this.displayStyle === 'linear'
     },
+    otherRepliesButtonPosition () {
+      return this.$store.getters.mergedConfig.conversationOtherRepliesButton
+    },
+    showOtherRepliesButtonBelowStatus () {
+      return this.otherRepliesButtonPosition === 'below'
+    },
+    showOtherRepliesButtonInsideStatus () {
+      return this.otherRepliesButtonPosition === 'inside'
+    },
     hideStatus () {
       if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
         return this.virtualHidden && this.$refs.statusComponent[0].suspendable
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index f0eb88c1..b3d97075 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -66,13 +66,14 @@
               :profile-user-id="profileUserId"
               class="conversation-status status-fadein panel-body"
 
-              :simple="treeViewIsSimple"
+              :simple-tree="treeViewIsSimple"
               :toggle-thread-display="toggleThreadDisplay"
               :thread-display-status="threadDisplayStatus"
               :show-thread-recursively="showThreadRecursively"
               :total-reply-count="totalReplyCount"
               :total-reply-depth="totalReplyDepth"
-              :dive="(!treeViewIsSimple) ? () => diveIntoStatus(status.id) : null"
+              :show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
+              :dive="() => diveIntoStatus(status.id)"
 
               :controlled-showing-tall="statusContentProperties[status.id].showingTall"
               :controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
@@ -85,7 +86,7 @@
               @toggleExpanded="toggleExpanded"
             />
             <div
-              v-if="getReplies(status.id).length > 1"
+              v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
               class="thread-ancestor-dive-box"
             >
               <div
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index c43abe8a..a963d204 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -25,6 +25,11 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.conversation_display_${mode}`)
       })),
+      conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.conversation_other_replies_button_${mode}`)
+      })),
       mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
         key: mode,
         value: mode,
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index f97d92c3..d5ae7810 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -161,19 +161,33 @@
             {{ $t('settings.conversation_display') }}
           </ChoiceSetting>
         </li>
-        <li v-if="conversationDisplay !== 'linear'">
-          <label for="maxDepthInThread">
-            {{ $t('settings.max_depth_in_thread') }}
-          </label>
-          <input
-            id="maxDepthInThread"
-            path.number="maxDepthInThread"
-            class="number-input"
-            type="number"
-            min="3"
-            step="1"
-          >
-        </li>
+        <ul
+          v-if="conversationDisplay !== 'linear'"
+          class="setting-list suboptions"
+        >
+          <li>
+            <label for="maxDepthInThread">
+              {{ $t('settings.max_depth_in_thread') }}
+            </label>
+            <input
+              id="maxDepthInThread"
+              path.number="maxDepthInThread"
+              class="number-input"
+              type="number"
+              min="3"
+              step="1"
+            >
+          </li>
+          <li>
+            <ChoiceSetting
+              id="conversationOtherRepliesButton"
+              path="conversationOtherRepliesButton"
+              :options="conversationOtherRepliesButtonOptions"
+            >
+              {{ $t('settings.conversation_other_replies_button') }}
+            </ChoiceSetting>
+          </li>
+        </ul>
         <li>
           <ChoiceSetting
             id="mentionLinkDisplay"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 15c2d029..700b9764 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -100,6 +100,7 @@ const Status = {
     'simpleTree',
     'controlledThreadDisplayStatus',
     'controlledToggleThreadDisplay',
+    'showOtherRepliesAsButton',
 
     'controlledShowingTall',
     'controlledToggleShowingTall',
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index e4812711..bc0aeaf0 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -346,7 +346,20 @@
             v-if="inConversation && !isPreview && replies && replies.length"
             class="replies"
           >
-            <span class="faint">{{ $t('status.replies_list') }}</span>
+            <button
+              v-if="showOtherRepliesAsButton"
+              class="button-unstyled -link faint"
+              :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })"
+              @click.prevent="dive"
+            >
+              {{ $t('status.replies_list') }}
+            </button>
+            <span
+              v-else
+              class="faint"
+            >
+              {{ $t('status.replies_list') }}
+            </span>
             <StatusPopover
               v-for="reply in replies"
               :key="reply.id"
diff --git a/src/modules/config.js b/src/modules/config.js
index b3c7e5ab..a111abca 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -13,6 +13,7 @@ export const multiChoiceProperties = [
   'postContentType',
   'subjectLineBehavior',
   'conversationDisplay', // tree | linear
+  'conversationOtherRepliesButton', // below | inside
   'mentionLinkDisplay' // short | full_for_remote | full
 ]
 
@@ -84,6 +85,7 @@ export const defaultState = {
   virtualScrolling: undefined, // instance default
   sensitiveByDefault: undefined, // instance default
   conversationDisplay: undefined, // instance default
+  conversationOtherRepliesButton: undefined, // instance default
   maxDepthInThread: 6
 }
 
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 853be737..cd17c23d 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -54,6 +54,7 @@ const defaultState = {
   virtualScrolling: true,
   sensitiveByDefault: false,
   conversationDisplay: 'simple_tree',
+  conversationOtherRepliesButton: 'below',
   maxDepthInThread: 6,
 
   // Nasty stuff
-- 
cgit v1.2.3-70-g09d2


From e7f5033c7d05038b116f7170fedc352a662cc646 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 6 Mar 2022 13:57:48 -0500
Subject: Split conversation display style into two different settings

linear => linear (now default)
simple_tree => tree / conversationTreeAdvanced=false
tree => tree / conversationTreeAdvanced=true
---
 src/components/conversation/conversation.js        | 4 ++--
 src/components/settings_modal/tabs/general_tab.js  | 2 +-
 src/components/settings_modal/tabs/general_tab.vue | 5 +++++
 src/modules/config.js                              | 1 +
 src/modules/instance.js                            | 3 ++-
 5 files changed, 11 insertions(+), 4 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index b9ebe7eb..46228e37 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -81,10 +81,10 @@ const conversation = {
       return this.$store.getters.mergedConfig.conversationDisplay
     },
     isTreeView () {
-      return this.displayStyle === 'tree' || this.displayStyle === 'simple_tree'
+      return !this.isLinearView
     },
     treeViewIsSimple () {
-      return this.displayStyle === 'simple_tree'
+      return !this.$store.getters.mergedConfig.conversationTreeAdvanced
     },
     isLinearView () {
       return this.displayStyle === 'linear'
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index a963d204..8ae0021c 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -20,7 +20,7 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
       })),
-      conversationDisplayOptions: ['tree', 'simple_tree', 'linear'].map(mode => ({
+      conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
         key: mode,
         value: mode,
         label: this.$t(`settings.conversation_display_${mode}`)
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index d5ae7810..28b39d7b 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -165,6 +165,11 @@
           v-if="conversationDisplay !== 'linear'"
           class="setting-list suboptions"
         >
+          <li>
+            <BooleanSetting path="conversationTreeAdvanced">
+              {{ $t('settings.tree_advanced') }}
+            </BooleanSetting>
+          </li>
           <li>
             <label for="maxDepthInThread">
               {{ $t('settings.max_depth_in_thread') }}
diff --git a/src/modules/config.js b/src/modules/config.js
index a111abca..8cb3097c 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -85,6 +85,7 @@ export const defaultState = {
   virtualScrolling: undefined, // instance default
   sensitiveByDefault: undefined, // instance default
   conversationDisplay: undefined, // instance default
+  conversationTreeAdvanced: undefined, // instance default
   conversationOtherRepliesButton: undefined, // instance default
   maxDepthInThread: 6
 }
diff --git a/src/modules/instance.js b/src/modules/instance.js
index cd17c23d..6c5cb4b1 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -53,7 +53,8 @@ const defaultState = {
   theme: 'pleroma-dark',
   virtualScrolling: true,
   sensitiveByDefault: false,
-  conversationDisplay: 'simple_tree',
+  conversationDisplay: 'linear',
+  conversationTreeAdvanced: false,
   conversationOtherRepliesButton: 'below',
   maxDepthInThread: 6,
 
-- 
cgit v1.2.3-70-g09d2


From d157f6738195325c59870fe02df8fa16985c84af Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 6 Mar 2022 14:13:35 -0500
Subject: Make fading ancestors optional

---
 src/components/conversation/conversation.js        | 3 +++
 src/components/conversation/conversation.vue       | 4 ++--
 src/components/settings_modal/tabs/general_tab.vue | 5 +++++
 src/modules/config.js                              | 1 +
 src/modules/instance.js                            | 1 +
 5 files changed, 12 insertions(+), 2 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 46228e37..89ff2f6f 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -89,6 +89,9 @@ const conversation = {
     isLinearView () {
       return this.displayStyle === 'linear'
     },
+    shouldFadeAncestors () {
+      return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
+    },
     otherRepliesButtonPosition () {
       return this.$store.getters.mergedConfig.conversationOtherRepliesButton
     },
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 73c613b9..3e96fba2 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -50,7 +50,7 @@
             v-for="status in ancestorsOf(diveRoot)"
             :key="status.id"
             class="thread-ancestor"
-            :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1}"
+            :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
           >
             <status
               ref="statusComponent"
@@ -210,7 +210,7 @@
     border-left: 2px solid var(--border, $fallback--border);
   }
 
-  .thread-ancestor .StatusContent {
+  .thread-ancestor.-faded .StatusContent {
     --link: var(--faintLink);
     --text: var(--faint);
     color: var(--text);
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 28b39d7b..6c108292 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -170,6 +170,11 @@
               {{ $t('settings.tree_advanced') }}
             </BooleanSetting>
           </li>
+          <li>
+            <BooleanSetting path="conversationTreeFadeAncestors">
+              {{ $t('settings.tree_fade_ancestors') }}
+            </BooleanSetting>
+          </li>
           <li>
             <label for="maxDepthInThread">
               {{ $t('settings.max_depth_in_thread') }}
diff --git a/src/modules/config.js b/src/modules/config.js
index 8cb3097c..acc71b80 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -87,6 +87,7 @@ export const defaultState = {
   conversationDisplay: undefined, // instance default
   conversationTreeAdvanced: undefined, // instance default
   conversationOtherRepliesButton: undefined, // instance default
+  conversationTreeFadeAncestors: undefined, // instance default
   maxDepthInThread: 6
 }
 
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 6c5cb4b1..f09dd0b3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -56,6 +56,7 @@ const defaultState = {
   conversationDisplay: 'linear',
   conversationTreeAdvanced: false,
   conversationOtherRepliesButton: 'below',
+  conversationTreeFadeAncestors: false,
   maxDepthInThread: 6,
 
   // Nasty stuff
-- 
cgit v1.2.3-70-g09d2


From 551b8f3690bab1f53ec22ccf6963009a30fa45d0 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Mon, 7 Mar 2022 20:02:53 -0500
Subject: Fix "max depth in thread" setting

---
 .../settings_modal/helpers/integer_setting.js      | 37 ++++++++++++++++++++++
 .../settings_modal/helpers/integer_setting.vue     | 20 ++++++++++++
 .../settings_modal/tabs/filtering_tab.js           |  4 ++-
 .../settings_modal/tabs/filtering_tab.vue          | 15 +++------
 src/components/settings_modal/tabs/general_tab.js  |  2 ++
 src/components/settings_modal/tabs/general_tab.vue | 15 +++------
 6 files changed, 72 insertions(+), 21 deletions(-)
 create mode 100644 src/components/settings_modal/helpers/integer_setting.js
 create mode 100644 src/components/settings_modal/helpers/integer_setting.vue

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js
new file mode 100644
index 00000000..fa6569ab
--- /dev/null
+++ b/src/components/settings_modal/helpers/integer_setting.js
@@ -0,0 +1,37 @@
+import { get, set } from 'lodash'
+import ModifiedIndicator from './modified_indicator.vue'
+export default {
+  components: {
+    ModifiedIndicator
+  },
+  props: {
+    path: String,
+    disabled: Boolean,
+    min: Number
+  },
+  computed: {
+    pathDefault () {
+      const [firstSegment, ...rest] = this.path.split('.')
+      return [firstSegment + 'DefaultValue', ...rest].join('.')
+    },
+    state () {
+      const value = get(this.$parent, this.path)
+      if (value === undefined) {
+        return this.defaultState
+      } else {
+        return value
+      }
+    },
+    defaultState () {
+      return get(this.$parent, this.pathDefault)
+    },
+    isChanged () {
+      return this.state !== this.defaultState
+    }
+  },
+  methods: {
+    update (e) {
+      set(this.$parent, this.path, parseInt(e.target.value))
+    }
+  }
+}
diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue
new file mode 100644
index 00000000..8284abcc
--- /dev/null
+++ b/src/components/settings_modal/helpers/integer_setting.vue
@@ -0,0 +1,20 @@
+<template>
+  <span class="IntegerSetting">
+    <label :for="path">
+      <slot />
+    </label>
+    <input
+      :id="path"
+      class="number-input"
+      type="number"
+      step="1"
+      :disabled="disabled"
+      :min="min || 0"
+      :value="state"
+      @change="update"
+    >
+    <ModifiedIndicator :changed="isChanged" />
+  </span>
+</template>
+
+<script src="./integer_setting.js"></script>
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 4eaf4217..73413b48 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -1,6 +1,7 @@
 import { filter, trim } from 'lodash'
 import BooleanSetting from '../helpers/boolean_setting.vue'
 import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
 
 import SharedComputedObject from '../helpers/shared_computed_object.js'
 
@@ -17,7 +18,8 @@ const FilteringTab = {
   },
   components: {
     BooleanSetting,
-    ChoiceSetting
+    ChoiceSetting,
+    IntegerSetting
   },
   computed: {
     ...SharedComputedObject(),
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 50ee20e0..eb1705e7 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -60,17 +60,12 @@
         </li>
         <h3>{{ $t('settings.attachments') }}</h3>
         <li>
-          <label for="maxThumbnails">
-            {{ $t('settings.max_thumbnails') }}
-          </label>
-          <input
-            id="maxThumbnails"
-            path.number="maxThumbnails"
-            class="number-input"
-            type="number"
-            min="0"
-            step="1"
+          <IntegerSetting
+            path="maxThumbnails"
+            :min="0"
           >
+            {{ $t('settings.max_thumbnails') }}
+          </IntegerSetting>
         </li>
         <li>
           <BooleanSetting path="hideAttachments">
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 8ae0021c..03ca99fa 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -1,5 +1,6 @@
 import BooleanSetting from '../helpers/boolean_setting.vue'
 import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
 import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
 
 import SharedComputedObject from '../helpers/shared_computed_object.js'
@@ -47,6 +48,7 @@ const GeneralTab = {
   components: {
     BooleanSetting,
     ChoiceSetting,
+    IntegerSetting,
     InterfaceLanguageSwitcher
   },
   computed: {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 6c108292..ba01fdeb 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -176,17 +176,12 @@
             </BooleanSetting>
           </li>
           <li>
-            <label for="maxDepthInThread">
-              {{ $t('settings.max_depth_in_thread') }}
-            </label>
-            <input
-              id="maxDepthInThread"
-              path.number="maxDepthInThread"
-              class="number-input"
-              type="number"
-              min="3"
-              step="1"
+            <IntegerSetting
+              path="maxDepthInThread"
+              :min="3"
             >
+              {{ $t('settings.max_depth_in_thread') }}
+            </IntegerSetting>
           </li>
           <li>
             <ChoiceSetting
-- 
cgit v1.2.3-70-g09d2


From 726fdbea15009e930ce47faed1e4001b16234a46 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 13 Mar 2022 10:51:13 +0200
Subject: remove "experimental" label from streaming api

---
 src/components/settings_modal/tabs/general_tab.vue | 4 ----
 src/i18n/en.json                                   | 1 -
 2 files changed, 5 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index d4196c3d..ceb61655 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -50,10 +50,6 @@
             expert="1"
           >
             {{ $t('settings.useStreamingApi') }}
-            <br>
-            <small>
-              {{ $t('settings.useStreamingApiWarning') }}
-            </small>
           </BooleanSetting>
         </li>
         <li>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index da5e662d..05549682 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -476,7 +476,6 @@
     "streaming": "Automatically show new posts when scrolled to the top",
     "user_mutes": "Users",
     "useStreamingApi": "Receive posts and notifications real-time",
-    "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
     "text": "Text",
     "theme": "Theme",
     "theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
-- 
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/components/settings_modal/tabs/general_tab.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 3d37b9d8e1cad78fb1f666b3cfb7f28b1fdc1c2d Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 12 Apr 2022 21:18:06 +0300
Subject: unified layout-setting code and made an option to control or disable
 third column behavior

---
 src/App.js                                         | 20 +++++++++---------
 src/boot/after_store.js                            |  9 +++-----
 src/components/settings_modal/tabs/general_tab.js  |  5 +++++
 src/components/settings_modal/tabs/general_tab.vue |  9 ++++++++
 src/modules/config.js                              |  4 ++++
 src/modules/interface.js                           | 24 +++++++++++++++++++---
 6 files changed, 52 insertions(+), 19 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/App.js b/src/App.js
index 9cfaf4fa..19c58595 100644
--- a/src/App.js
+++ b/src/App.js
@@ -98,22 +98,22 @@ export default {
     },
     layoutType () { return this.$store.state.interface.layoutType },
     privateMode () { return this.$store.state.instance.private },
-    reverseLayout () { return this.$store.getters.mergedConfig.sidebarRight },
+    reverseLayout () {
+      const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
+      if (this.layoutType !== 'wide') {
+        return reverseSetting
+      } else {
+        return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
+      }
+    },
     noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
     showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
     ...mapGetters(['mergedConfig'])
   },
   methods: {
     updateMobileState () {
-      const mobileLayout = windowWidth() <= 800
-      const wideLayout = windowWidth() >= 1300
-      const layoutHeight = windowHeight()
-      const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
-      const changed = layoutType !== this.layoutType
-      if (changed) {
-        this.$store.dispatch('setLayoutType', layoutType)
-      }
-      this.$store.dispatch('setLayoutHeight', layoutHeight)
+      this.$store.dispatch('setLayoutWidth', windowWidth())
+      this.$store.dispatch('setLayoutHeight', windowHeight())
     }
   }
 }
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index ce96141d..f655c38f 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -8,7 +8,7 @@ import App from '../App.vue'
 import routes from './routes'
 import VBodyScrollLock from 'src/directives/body_scroll_lock'
 
-import { windowWidth } from '../services/window_utils/window_utils'
+import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
 import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
 import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
@@ -332,11 +332,8 @@ const checkOAuthToken = async ({ store }) => {
 }
 
 const afterStoreSetup = async ({ store, i18n }) => {
-  // TODO cleanup copypasta
-  const mobileLayout = windowWidth() <= 800
-  const wideLayout = windowWidth() >= 1300
-  const layoutType = wideLayout ? 'wide' : (mobileLayout ? 'mobile' : 'normal')
-  store.dispatch('setLayoutType', layoutType)
+  store.dispatch('setLayoutWidth', windowWidth())
+  store.dispatch('setLayoutHeight', windowHeight())
 
   FaviconService.initFaviconService()
 
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 62d86176..89505cb3 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -38,6 +38,11 @@ const GeneralTab = {
         value: mode,
         label: this.$t(`settings.mention_link_display_${mode}`)
       })),
+      thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
+        key: mode,
+        value: mode,
+        label: this.$t(`settings.conversation_display_${mode}`)
+      })),
       loopSilentAvailable:
       // Firefox
       Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index e29bc7cf..db8718cf 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -70,6 +70,15 @@
             {{ $t('settings.show_scrollbars') }}
           </BooleanSetting>
         </li>
+        <li>
+          <ChoiceSetting
+            id="thirdColumnMode"
+            path="thirdColumnMode"
+            :options="thirdColumnModeOptions"
+          >
+            {{ $t('settings.third_column_mode') }}
+          </ChoiceSetting>
+        </li>
         <li>
           <BooleanSetting
             path="alwaysShowNewPostButton"
diff --git a/src/modules/config.js b/src/modules/config.js
index 6d2e6ce0..c0d5c3c2 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -46,6 +46,7 @@ export const defaultState = {
   pauseOnUnfocused: true,
   stopGifs: true,
   replyVisibility: 'all',
+  thirdColumnMode: 'notifications',
   notificationVisibility: {
     follows: true,
     mentions: true,
@@ -165,6 +166,9 @@ const config = {
         case 'interfaceLanguage':
           messages.setLanguage(this.getters.i18n, value)
           break
+        case 'thirdColumnMode':
+          dispatch('setLayoutWidth', undefined)
+          break
       }
     }
   }
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 9df72b88..ed31b6ff 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -72,6 +72,9 @@ const interfaceMod = {
     setLayoutHeight (state, value) {
       state.layoutHeight = value
     },
+    setLayoutWidth (state, value) {
+      state.layoutWidth = value
+    },
     setLastTimeline (state, value) {
       state.lastTimeline = value
     }
@@ -86,9 +89,6 @@ const interfaceMod = {
     setNotificationPermission ({ commit }, permission) {
       commit('setNotificationPermission', permission)
     },
-    setLayoutType ({ commit }, value) {
-      commit('setLayoutType', value)
-    },
     closeSettingsModal ({ commit }) {
       commit('closeSettingsModal')
     },
@@ -133,6 +133,24 @@ const interfaceMod = {
     setLayoutHeight ({ commit }, value) {
       commit('setLayoutHeight', value)
     },
+    // value is optional, assuming it was cached prior
+    setLayoutWidth ({ commit, state, rootGetters }, value) {
+      let width = value
+      if (value !== undefined) {
+        commit('setLayoutWidth', value)
+      } else {
+        width = state.layoutWidth
+      }
+      const mobileLayout = width <= 800
+      const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
+      const { thirdColumnMode } = rootGetters.mergedConfig
+      if (thirdColumnMode === 'none') {
+        commit('setLayoutType', normalOrMobile)
+      } else {
+        const wideLayout = width >= 1300
+        commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
+      }
+    },
     setLastTimeline ({ commit }, value) {
       commit('setLastTimeline', value)
     }
-- 
cgit v1.2.3-70-g09d2


From a5e861cd019eb7770b8524ecdb6237f33fd18d12 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 26 Mar 2022 12:21:34 -0400
Subject: Set userLanguage cookie when setting interface language

---
 package.json                                       |  1 +
 .../interface_language_switcher.vue                | 26 ++++++++-------------
 src/components/settings_modal/tabs/general_tab.js  |  6 +++++
 src/components/settings_modal/tabs/general_tab.vue |  5 +++-
 src/modules/config.js                              |  6 +++++
 src/services/locale/locale.service.js              | 27 ++++++++++++++++++++--
 yarn.lock                                          |  5 ++++
 7 files changed, 57 insertions(+), 19 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/package.json b/package.json
index e9a246b3..ab9043d3 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
     "cropperjs": "1.5.12",
     "diff": "3.5.0",
     "escape-html": "1.0.3",
+    "js-cookie": "^3.0.1",
     "localforage": "1.10.0",
     "parse-link-header": "1.0.1",
     "phoenix": "1.6.2",
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 6d1f83c4..757ec01f 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -6,7 +6,7 @@
     {{ ' ' }}
     <Select
       id="interface-language-switcher"
-      v-model="language"
+      v-model="controlledLanguage"
     >
       <option
         v-for="lang in languages"
@@ -20,39 +20,33 @@
 </template>
 
 <script>
-import languagesObject from '../../i18n/messages'
 import localeService from '../../services/locale/locale.service.js'
-import ISO6391 from 'iso-639-1'
-import _ from 'lodash'
 import Select from '../select/select.vue'
 
 export default {
   components: {
     Select
   },
+  props: {
+    language: String,
+    setLanguage: Function
+  },
   computed: {
     languages () {
-      return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
+      return localeService.languages
     },
 
-    language: {
-      get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+    controlledLanguage: {
+      get: function () { return this.language },
       set: function (val) {
-        this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+        this.setLanguage(val)
       }
     }
   },
 
   methods: {
     getLanguageName (code) {
-      const specialLanguageNames = {
-        'ja_easy': 'やさしいにほんご',
-        'zh': '简体中文',
-        'zh_Hant': '繁體中文'
-      }
-      const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
-      const browserLocale = localeService.internalToBrowserLocale(code)
-      return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
+      return localeService.getLanguageName(code)
     }
   }
 }
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 62d86176..de925155 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -72,6 +72,12 @@ const GeneralTab = {
         !this.$store.state.users.currentUser.background_image
     },
     instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
+    language: {
+      get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+      set: function (val) {
+        this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+      }
+    },
     ...SharedComputedObject()
   },
   methods: {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index a2c6bffa..5bc0f516 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -4,7 +4,10 @@
       <h2>{{ $t('settings.interface') }}</h2>
       <ul class="setting-list">
         <li>
-          <interface-language-switcher />
+          <interface-language-switcher
+            :language="language"
+            :set-language="val => language = val"
+          />
         </li>
         <li v-if="instanceSpecificPanelPresent">
           <BooleanSetting path="hideISP">
diff --git a/src/modules/config.js b/src/modules/config.js
index ff5ef270..eeefd896 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,5 +1,10 @@
+import { set, delete as del } from 'vue'
+import Cookies from 'js-cookie'
 import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
 import messages from '../i18n/messages'
+import localeService from '../services/locale/locale.service.js'
+
+const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
 
 const browserLocale = (window.navigator.language || 'en').split('-')[0]
 
@@ -162,6 +167,7 @@ const config = {
           break
         case 'interfaceLanguage':
           messages.setLanguage(this.getters.i18n, value)
+          Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
           break
       }
     }
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
index 5be99d81..8cef2522 100644
--- a/src/services/locale/locale.service.js
+++ b/src/services/locale/locale.service.js
@@ -1,12 +1,35 @@
+import languagesObject from '../../i18n/messages'
+import ISO6391 from 'iso-639-1'
+import _ from 'lodash'
+
 const specialLanguageCodes = {
   'ja_easy': 'ja',
-  'zh_Hant': 'zh-HANT'
+  'zh_Hant': 'zh-HANT',
+  'zh': 'zh-Hans'
 }
 
 const internalToBrowserLocale = code => specialLanguageCodes[code] || code
 
+const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
+
+const getLanguageName = (code) => {
+  const specialLanguageNames = {
+    '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 localeService = {
-  internalToBrowserLocale
+  internalToBrowserLocale,
+  internalToBackendLocale,
+  languages,
+  getLanguageName
 }
 
 export default localeService
diff --git a/yarn.lock b/yarn.lock
index 042d87ac..cd96543b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5722,6 +5722,11 @@ js-base64@^2.1.9:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e"
 
+js-cookie@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
+  integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-- 
cgit v1.2.3-70-g09d2


From d39de3022108c4a42d20fe2a38265179ae0062f9 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sat, 26 Mar 2022 12:39:26 -0400
Subject: Add config opts for email language

---
 .../interface_language_switcher.vue                | 16 +++++++--
 src/components/settings_modal/tabs/general_tab.vue |  1 +
 src/components/settings_modal/tabs/profile_tab.js  | 38 ++++++++++++++--------
 src/components/settings_modal/tabs/profile_tab.vue |  7 ++++
 src/services/api/api.service.js                    |  1 +
 5 files changed, 46 insertions(+), 17 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 757ec01f..078535dc 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <label for="interface-language-switcher">
-      {{ $t('settings.interfaceLanguage') }}
+      {{ promptText }}
     </label>
     {{ ' ' }}
     <Select
@@ -28,8 +28,18 @@ export default {
     Select
   },
   props: {
-    language: String,
-    setLanguage: Function
+    promptText: {
+      type: String,
+      required: true
+    },
+    language: {
+      type: String,
+      required: true
+    },
+    setLanguage: {
+      type: String,
+      required: true
+    }
   },
   computed: {
     languages () {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 5bc0f516..39149d7f 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -5,6 +5,7 @@
       <ul class="setting-list">
         <li>
           <interface-language-switcher
+            :prompt-text="$t('settings.interfaceLanguage')"
             :language="language"
             :set-language="val => language = val"
           />
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index bee8a7bb..953f17f0 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -8,8 +8,10 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
 import suggestor from 'src/components/emoji_input/suggestor.js'
 import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
 import BooleanSetting from '../helpers/boolean_setting.vue'
 import SharedComputedObject from '../helpers/shared_computed_object.js'
+import localeService from 'src/services/locale/locale.service.js'
 
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -40,7 +42,8 @@ const ProfileTab = {
       banner: null,
       bannerPreview: null,
       background: null,
-      backgroundPreview: null
+      backgroundPreview: null,
+      emailLanguage: this.$store.state.users.currentUser.language
     }
   },
   components: {
@@ -50,7 +53,8 @@ const ProfileTab = {
     Autosuggest,
     ProgressButton,
     Checkbox,
-    BooleanSetting
+    BooleanSetting,
+    InterfaceLanguageSwitcher
   },
   computed: {
     user () {
@@ -111,19 +115,25 @@ const ProfileTab = {
   },
   methods: {
     updateProfile () {
+      const params = {
+        note: this.newBio,
+        locked: this.newLocked,
+        // Backend notation.
+        /* eslint-disable camelcase */
+        display_name: this.newName,
+        fields_attributes: this.newFields.filter(el => el != null),
+        bot: this.bot,
+        show_role: this.showRole,
+        /* eslint-enable camelcase */
+      }
+
+      if (this.emailLanguage) {
+        params.language = localeService.internalToBackendLocale(this.emailLanguage)
+      }
+
       this.$store.state.api.backendInteractor
-        .updateProfile({
-          params: {
-            note: this.newBio,
-            locked: this.newLocked,
-            // Backend notation.
-            /* eslint-disable camelcase */
-            display_name: this.newName,
-            fields_attributes: this.newFields.filter(el => el != null),
-            bot: this.bot,
-            show_role: this.showRole
-            /* eslint-enable camelcase */
-          } }).then((user) => {
+        .updateProfile({ params })
+        .then((user) => {
           this.newFields.splice(user.fields.length)
           merge(this.newFields, user.fields)
           this.$store.commit('addNewUsers', [user])
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 881016fb..4cd93772 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -89,6 +89,13 @@
           {{ $t('settings.bot') }}
         </Checkbox>
       </p>
+      <p>
+        <interface-language-switcher
+          :prompt-text="$t('settings.email_language')"
+          :language="emailLanguage"
+          :set-language="val => emailLanguage = val"
+        />
+      </p>
       <button
         :disabled="newName && newName.length === 0"
         class="btn button-default"
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 436b8b0a..1b1722ab 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -191,6 +191,7 @@ const updateProfile = ({ credentials, params }) => {
 // homepage
 // location
 // token
+// language
 const register = ({ params, credentials }) => {
   const { nickname, ...rest } = params
   return fetch(MASTODON_REGISTRATION_URL, {
-- 
cgit v1.2.3-70-g09d2


From edbbbaad48311d5d791183f724e819926c09e638 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 9 May 2022 23:24:35 +0300
Subject: don't use wide mode for anon viewers

---
 src/components/settings_modal/tabs/general_tab.vue | 1 +
 src/modules/interface.js                           | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index db8718cf..79c69449 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -72,6 +72,7 @@
         </li>
         <li>
           <ChoiceSetting
+            v-if="user"
             id="thirdColumnMode"
             path="thirdColumnMode"
             :options="thirdColumnModeOptions"
diff --git a/src/modules/interface.js b/src/modules/interface.js
index ed31b6ff..a86193ea 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -134,7 +134,7 @@ const interfaceMod = {
       commit('setLayoutHeight', value)
     },
     // value is optional, assuming it was cached prior
-    setLayoutWidth ({ commit, state, rootGetters }, value) {
+    setLayoutWidth ({ commit, state, rootGetters, rootState }, value) {
       let width = value
       if (value !== undefined) {
         commit('setLayoutWidth', value)
@@ -144,7 +144,7 @@ const interfaceMod = {
       const mobileLayout = width <= 800
       const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
       const { thirdColumnMode } = rootGetters.mergedConfig
-      if (thirdColumnMode === 'none') {
+      if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
         commit('setLayoutType', normalOrMobile)
       } else {
         const wideLayout = width >= 1300
-- 
cgit v1.2.3-70-g09d2


From 069cf3fa87c0d29803275fe66864d5e439cad309 Mon Sep 17 00:00:00 2001
From: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Sun, 22 May 2022 16:38:09 +0000
Subject: Revert "Merge branch 'from/develop/tusooa/lang-opts' into 'develop'"

This reverts merge request !1494
---
 package.json                                       |  1 -
 .../interface_language_switcher.vue                | 38 ++++++++++------------
 src/components/registration/registration.js        | 14 ++------
 src/components/registration/registration.vue       | 12 -------
 src/components/settings_modal/tabs/general_tab.js  |  6 ----
 src/components/settings_modal/tabs/general_tab.vue |  6 +---
 src/components/settings_modal/tabs/profile_tab.js  | 38 ++++++++--------------
 src/components/settings_modal/tabs/profile_tab.vue |  7 ----
 src/i18n/en.json                                   |  4 +--
 src/modules/config.js                              |  5 ---
 src/services/api/api.service.js                    |  1 -
 src/services/locale/locale.service.js              | 27 ++-------------
 yarn.lock                                          |  5 ---
 13 files changed, 37 insertions(+), 127 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/package.json b/package.json
index 58aeb363..b5ece450 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,6 @@
     "cropperjs": "1.5.12",
     "diff": "3.5.0",
     "escape-html": "1.0.3",
-    "js-cookie": "^3.0.1",
     "localforage": "1.10.0",
     "parse-link-header": "1.0.1",
     "phoenix": "1.6.2",
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 7ad1fe2e..6d1f83c4 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -1,12 +1,12 @@
 <template>
   <div>
     <label for="interface-language-switcher">
-      {{ promptText }}
+      {{ $t('settings.interfaceLanguage') }}
     </label>
     {{ ' ' }}
     <Select
       id="interface-language-switcher"
-      v-model="controlledLanguage"
+      v-model="language"
     >
       <option
         v-for="lang in languages"
@@ -20,43 +20,39 @@
 </template>
 
 <script>
+import languagesObject from '../../i18n/messages'
 import localeService from '../../services/locale/locale.service.js'
+import ISO6391 from 'iso-639-1'
+import _ from 'lodash'
 import Select from '../select/select.vue'
 
 export default {
   components: {
     Select
   },
-  props: {
-    promptText: {
-      type: String,
-      required: true
-    },
-    language: {
-      type: String,
-      required: true
-    },
-    setLanguage: {
-      type: Function,
-      required: true
-    }
-  },
   computed: {
     languages () {
-      return localeService.languages
+      return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
     },
 
-    controlledLanguage: {
-      get: function () { return this.language },
+    language: {
+      get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
       set: function (val) {
-        this.setLanguage(val)
+        this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
       }
     }
   },
 
   methods: {
     getLanguageName (code) {
-      return localeService.getLanguageName(code)
+      const specialLanguageNames = {
+        'ja_easy': 'やさしいにほんご',
+        'zh': '简体中文',
+        'zh_Hant': '繁體中文'
+      }
+      const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
+      const browserLocale = localeService.internalToBrowserLocale(code)
+      return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
     }
   }
 }
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 6eb316d0..a3ef0f04 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,8 +1,6 @@
 import useVuelidate from '@vuelidate/core'
 import { required, requiredIf, sameAs } from '@vuelidate/validators'
 import { mapActions, mapState } from 'vuex'
-import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
-import localeService from '../../services/locale/locale.service.js'
 
 const registration = {
   setup () { return { v$: useVuelidate() } },
@@ -13,14 +11,10 @@ const registration = {
       username: '',
       password: '',
       confirm: '',
-      reason: '',
-      language: ''
+      reason: ''
     },
     captcha: {}
   }),
-  components: {
-    InterfaceLanguageSwitcher
-  },
   validations () {
     return {
       user: {
@@ -32,8 +26,7 @@ const registration = {
           required,
           sameAs: sameAs(this.user.password)
         },
-        reason: { required: requiredIf(() => this.accountApprovalRequired) },
-        language: {}
+        reason: { required: requiredIf(() => this.accountApprovalRequired) }
       }
     }
   },
@@ -71,9 +64,6 @@ const registration = {
       this.user.captcha_solution = this.captcha.solution
       this.user.captcha_token = this.captcha.token
       this.user.captcha_answer_data = this.captcha.answer_data
-      if (this.user.language) {
-        this.user.language = localeService.internalToBackendLocale(this.user.language)
-      }
 
       this.v$.$touch()
 
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 6fa70a09..3d409109 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -162,18 +162,6 @@
               </ul>
             </div>
 
-            <div
-              class="form-group"
-              :class="{ 'form-group--error': $v.user.language.$error }"
-            >
-              <interface-language-switcher
-                for="email-language"
-                :prompt-text="$t('registration.email_language')"
-                :language="$v.user.language.$model"
-                :set-language="val => $v.user.language.$model = val"
-              />
-            </div>
-
             <div
               v-if="accountApprovalRequired"
               class="form-group"
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index de925155..62d86176 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -72,12 +72,6 @@ const GeneralTab = {
         !this.$store.state.users.currentUser.background_image
     },
     instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
-    language: {
-      get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
-      set: function (val) {
-        this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
-      }
-    },
     ...SharedComputedObject()
   },
   methods: {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 39149d7f..a2c6bffa 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -4,11 +4,7 @@
       <h2>{{ $t('settings.interface') }}</h2>
       <ul class="setting-list">
         <li>
-          <interface-language-switcher
-            :prompt-text="$t('settings.interfaceLanguage')"
-            :language="language"
-            :set-language="val => language = val"
-          />
+          <interface-language-switcher />
         </li>
         <li v-if="instanceSpecificPanelPresent">
           <BooleanSetting path="hideISP">
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 8781bb91..f2b48b4b 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -8,10 +8,8 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
 import suggestor from 'src/components/emoji_input/suggestor.js'
 import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
-import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
 import BooleanSetting from '../helpers/boolean_setting.vue'
 import SharedComputedObject from '../helpers/shared_computed_object.js'
-import localeService from 'src/services/locale/locale.service.js'
 
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -42,8 +40,7 @@ const ProfileTab = {
       banner: null,
       bannerPreview: null,
       background: null,
-      backgroundPreview: null,
-      emailLanguage: this.$store.state.users.currentUser.language || ''
+      backgroundPreview: null
     }
   },
   components: {
@@ -53,8 +50,7 @@ const ProfileTab = {
     Autosuggest,
     ProgressButton,
     Checkbox,
-    BooleanSetting,
-    InterfaceLanguageSwitcher
+    BooleanSetting
   },
   computed: {
     user () {
@@ -115,25 +111,19 @@ const ProfileTab = {
   },
   methods: {
     updateProfile () {
-      const params = {
-        note: this.newBio,
-        locked: this.newLocked,
-        // Backend notation.
-        /* eslint-disable camelcase */
-        display_name: this.newName,
-        fields_attributes: this.newFields.filter(el => el != null),
-        bot: this.bot,
-        show_role: this.showRole
-        /* eslint-enable camelcase */
-      }
-
-      if (this.emailLanguage) {
-        params.language = localeService.internalToBackendLocale(this.emailLanguage)
-      }
-
       this.$store.state.api.backendInteractor
-        .updateProfile({ params })
-        .then((user) => {
+        .updateProfile({
+          params: {
+            note: this.newBio,
+            locked: this.newLocked,
+            // Backend notation.
+            /* eslint-disable camelcase */
+            display_name: this.newName,
+            fields_attributes: this.newFields.filter(el => el != null),
+            bot: this.bot,
+            show_role: this.showRole
+            /* eslint-enable camelcase */
+          } }).then((user) => {
           this.newFields.splice(user.fields.length)
           merge(this.newFields, user.fields)
           this.$store.commit('addNewUsers', [user])
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 4cd93772..881016fb 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -89,13 +89,6 @@
           {{ $t('settings.bot') }}
         </Checkbox>
       </p>
-      <p>
-        <interface-language-switcher
-          :prompt-text="$t('settings.email_language')"
-          :language="emailLanguage"
-          :set-language="val => emailLanguage = val"
-        />
-      </p>
       <button
         :disabled="newName && newName.length === 0"
         class="btn button-default"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a3174492..733df0a0 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -255,8 +255,7 @@
       "password_required": "cannot be left blank",
       "password_confirmation_required": "cannot be left blank",
       "password_confirmation_match": "should be the same as password"
-    },
-    "email_language": "In which language do you want to receive emails from the server?"
+    }
   },
   "remote_user_resolver": {
     "remote_user_resolver": "Remote user resolver",
@@ -305,7 +304,6 @@
     "avatarRadius": "Avatars",
     "background": "Background",
     "bio": "Bio",
-    "email_language": "Language for receiving emails from the server",
     "block_export": "Block export",
     "block_export_button": "Export your blocks to a csv file",
     "block_import": "Block import",
diff --git a/src/modules/config.js b/src/modules/config.js
index e0401339..13f1045b 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,9 +1,5 @@
-import Cookies from 'js-cookie'
 import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
 import messages from '../i18n/messages'
-import localeService from '../services/locale/locale.service.js'
-
-const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
 
 const browserLocale = (window.navigator.language || 'en').split('-')[0]
 
@@ -167,7 +163,6 @@ const config = {
           break
         case 'interfaceLanguage':
           messages.setLanguage(this.getters.i18n, value)
-          Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
           break
       }
     }
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ca84ba2c..50fa3d50 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -197,7 +197,6 @@ const updateProfile = ({ credentials, params }) => {
 // homepage
 // location
 // token
-// language
 const register = ({ params, credentials }) => {
   const { nickname, ...rest } = params
   return fetch(MASTODON_REGISTRATION_URL, {
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
index 8cef2522..5be99d81 100644
--- a/src/services/locale/locale.service.js
+++ b/src/services/locale/locale.service.js
@@ -1,35 +1,12 @@
-import languagesObject from '../../i18n/messages'
-import ISO6391 from 'iso-639-1'
-import _ from 'lodash'
-
 const specialLanguageCodes = {
   'ja_easy': 'ja',
-  'zh_Hant': 'zh-HANT',
-  'zh': 'zh-Hans'
+  'zh_Hant': 'zh-HANT'
 }
 
 const internalToBrowserLocale = code => specialLanguageCodes[code] || code
 
-const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
-
-const getLanguageName = (code) => {
-  const specialLanguageNames = {
-    '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 localeService = {
-  internalToBrowserLocale,
-  internalToBackendLocale,
-  languages,
-  getLanguageName
+  internalToBrowserLocale
 }
 
 export default localeService
diff --git a/yarn.lock b/yarn.lock
index e413b6d1..a898a932 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5722,11 +5722,6 @@ js-base64@^2.1.9:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e"
 
-js-cookie@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
-  integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
-
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-- 
cgit v1.2.3-70-g09d2


From 9725443e60bc6e147fdc8d195dab048d1d5aa461 Mon Sep 17 00:00:00 2001
From: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Sun, 22 May 2022 16:40:59 +0000
Subject: Revert "Merge branch 'revert-a88abc7e' into 'develop'"

This reverts merge request !1526
---
 package.json                                       |  1 +
 .../interface_language_switcher.vue                | 38 ++++++++++++----------
 src/components/registration/registration.js        | 14 ++++++--
 src/components/registration/registration.vue       | 12 +++++++
 src/components/settings_modal/tabs/general_tab.js  |  6 ++++
 src/components/settings_modal/tabs/general_tab.vue |  6 +++-
 src/components/settings_modal/tabs/profile_tab.js  | 38 ++++++++++++++--------
 src/components/settings_modal/tabs/profile_tab.vue |  7 ++++
 src/i18n/en.json                                   |  4 ++-
 src/modules/config.js                              |  5 +++
 src/services/api/api.service.js                    |  1 +
 src/services/locale/locale.service.js              | 27 +++++++++++++--
 yarn.lock                                          |  5 +++
 13 files changed, 127 insertions(+), 37 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/package.json b/package.json
index b5ece450..58aeb363 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
     "cropperjs": "1.5.12",
     "diff": "3.5.0",
     "escape-html": "1.0.3",
+    "js-cookie": "^3.0.1",
     "localforage": "1.10.0",
     "parse-link-header": "1.0.1",
     "phoenix": "1.6.2",
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 6d1f83c4..7ad1fe2e 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -1,12 +1,12 @@
 <template>
   <div>
     <label for="interface-language-switcher">
-      {{ $t('settings.interfaceLanguage') }}
+      {{ promptText }}
     </label>
     {{ ' ' }}
     <Select
       id="interface-language-switcher"
-      v-model="language"
+      v-model="controlledLanguage"
     >
       <option
         v-for="lang in languages"
@@ -20,39 +20,43 @@
 </template>
 
 <script>
-import languagesObject from '../../i18n/messages'
 import localeService from '../../services/locale/locale.service.js'
-import ISO6391 from 'iso-639-1'
-import _ from 'lodash'
 import Select from '../select/select.vue'
 
 export default {
   components: {
     Select
   },
+  props: {
+    promptText: {
+      type: String,
+      required: true
+    },
+    language: {
+      type: String,
+      required: true
+    },
+    setLanguage: {
+      type: Function,
+      required: true
+    }
+  },
   computed: {
     languages () {
-      return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
+      return localeService.languages
     },
 
-    language: {
-      get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+    controlledLanguage: {
+      get: function () { return this.language },
       set: function (val) {
-        this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+        this.setLanguage(val)
       }
     }
   },
 
   methods: {
     getLanguageName (code) {
-      const specialLanguageNames = {
-        'ja_easy': 'やさしいにほんご',
-        'zh': '简体中文',
-        'zh_Hant': '繁體中文'
-      }
-      const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
-      const browserLocale = localeService.internalToBrowserLocale(code)
-      return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
+      return localeService.getLanguageName(code)
     }
   }
 }
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index a3ef0f04..6eb316d0 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,6 +1,8 @@
 import useVuelidate from '@vuelidate/core'
 import { required, requiredIf, sameAs } from '@vuelidate/validators'
 import { mapActions, mapState } from 'vuex'
+import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
+import localeService from '../../services/locale/locale.service.js'
 
 const registration = {
   setup () { return { v$: useVuelidate() } },
@@ -11,10 +13,14 @@ const registration = {
       username: '',
       password: '',
       confirm: '',
-      reason: ''
+      reason: '',
+      language: ''
     },
     captcha: {}
   }),
+  components: {
+    InterfaceLanguageSwitcher
+  },
   validations () {
     return {
       user: {
@@ -26,7 +32,8 @@ const registration = {
           required,
           sameAs: sameAs(this.user.password)
         },
-        reason: { required: requiredIf(() => this.accountApprovalRequired) }
+        reason: { required: requiredIf(() => this.accountApprovalRequired) },
+        language: {}
       }
     }
   },
@@ -64,6 +71,9 @@ const registration = {
       this.user.captcha_solution = this.captcha.solution
       this.user.captcha_token = this.captcha.token
       this.user.captcha_answer_data = this.captcha.answer_data
+      if (this.user.language) {
+        this.user.language = localeService.internalToBackendLocale(this.user.language)
+      }
 
       this.v$.$touch()
 
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 3d409109..6fa70a09 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -162,6 +162,18 @@
               </ul>
             </div>
 
+            <div
+              class="form-group"
+              :class="{ 'form-group--error': $v.user.language.$error }"
+            >
+              <interface-language-switcher
+                for="email-language"
+                :prompt-text="$t('registration.email_language')"
+                :language="$v.user.language.$model"
+                :set-language="val => $v.user.language.$model = val"
+              />
+            </div>
+
             <div
               v-if="accountApprovalRequired"
               class="form-group"
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 62d86176..de925155 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -72,6 +72,12 @@ const GeneralTab = {
         !this.$store.state.users.currentUser.background_image
     },
     instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
+    language: {
+      get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+      set: function (val) {
+        this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+      }
+    },
     ...SharedComputedObject()
   },
   methods: {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index a2c6bffa..39149d7f 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -4,7 +4,11 @@
       <h2>{{ $t('settings.interface') }}</h2>
       <ul class="setting-list">
         <li>
-          <interface-language-switcher />
+          <interface-language-switcher
+            :prompt-text="$t('settings.interfaceLanguage')"
+            :language="language"
+            :set-language="val => language = val"
+          />
         </li>
         <li v-if="instanceSpecificPanelPresent">
           <BooleanSetting path="hideISP">
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index f2b48b4b..8781bb91 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -8,8 +8,10 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
 import suggestor from 'src/components/emoji_input/suggestor.js'
 import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
 import BooleanSetting from '../helpers/boolean_setting.vue'
 import SharedComputedObject from '../helpers/shared_computed_object.js'
+import localeService from 'src/services/locale/locale.service.js'
 
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
@@ -40,7 +42,8 @@ const ProfileTab = {
       banner: null,
       bannerPreview: null,
       background: null,
-      backgroundPreview: null
+      backgroundPreview: null,
+      emailLanguage: this.$store.state.users.currentUser.language || ''
     }
   },
   components: {
@@ -50,7 +53,8 @@ const ProfileTab = {
     Autosuggest,
     ProgressButton,
     Checkbox,
-    BooleanSetting
+    BooleanSetting,
+    InterfaceLanguageSwitcher
   },
   computed: {
     user () {
@@ -111,19 +115,25 @@ const ProfileTab = {
   },
   methods: {
     updateProfile () {
+      const params = {
+        note: this.newBio,
+        locked: this.newLocked,
+        // Backend notation.
+        /* eslint-disable camelcase */
+        display_name: this.newName,
+        fields_attributes: this.newFields.filter(el => el != null),
+        bot: this.bot,
+        show_role: this.showRole
+        /* eslint-enable camelcase */
+      }
+
+      if (this.emailLanguage) {
+        params.language = localeService.internalToBackendLocale(this.emailLanguage)
+      }
+
       this.$store.state.api.backendInteractor
-        .updateProfile({
-          params: {
-            note: this.newBio,
-            locked: this.newLocked,
-            // Backend notation.
-            /* eslint-disable camelcase */
-            display_name: this.newName,
-            fields_attributes: this.newFields.filter(el => el != null),
-            bot: this.bot,
-            show_role: this.showRole
-            /* eslint-enable camelcase */
-          } }).then((user) => {
+        .updateProfile({ params })
+        .then((user) => {
           this.newFields.splice(user.fields.length)
           merge(this.newFields, user.fields)
           this.$store.commit('addNewUsers', [user])
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 881016fb..4cd93772 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -89,6 +89,13 @@
           {{ $t('settings.bot') }}
         </Checkbox>
       </p>
+      <p>
+        <interface-language-switcher
+          :prompt-text="$t('settings.email_language')"
+          :language="emailLanguage"
+          :set-language="val => emailLanguage = val"
+        />
+      </p>
       <button
         :disabled="newName && newName.length === 0"
         class="btn button-default"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 733df0a0..a3174492 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -255,7 +255,8 @@
       "password_required": "cannot be left blank",
       "password_confirmation_required": "cannot be left blank",
       "password_confirmation_match": "should be the same as password"
-    }
+    },
+    "email_language": "In which language do you want to receive emails from the server?"
   },
   "remote_user_resolver": {
     "remote_user_resolver": "Remote user resolver",
@@ -304,6 +305,7 @@
     "avatarRadius": "Avatars",
     "background": "Background",
     "bio": "Bio",
+    "email_language": "Language for receiving emails from the server",
     "block_export": "Block export",
     "block_export_button": "Export your blocks to a csv file",
     "block_import": "Block import",
diff --git a/src/modules/config.js b/src/modules/config.js
index 13f1045b..e0401339 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,5 +1,9 @@
+import Cookies from 'js-cookie'
 import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
 import messages from '../i18n/messages'
+import localeService from '../services/locale/locale.service.js'
+
+const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
 
 const browserLocale = (window.navigator.language || 'en').split('-')[0]
 
@@ -163,6 +167,7 @@ const config = {
           break
         case 'interfaceLanguage':
           messages.setLanguage(this.getters.i18n, value)
+          Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
           break
       }
     }
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 50fa3d50..ca84ba2c 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -197,6 +197,7 @@ const updateProfile = ({ credentials, params }) => {
 // homepage
 // location
 // token
+// language
 const register = ({ params, credentials }) => {
   const { nickname, ...rest } = params
   return fetch(MASTODON_REGISTRATION_URL, {
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
index 5be99d81..8cef2522 100644
--- a/src/services/locale/locale.service.js
+++ b/src/services/locale/locale.service.js
@@ -1,12 +1,35 @@
+import languagesObject from '../../i18n/messages'
+import ISO6391 from 'iso-639-1'
+import _ from 'lodash'
+
 const specialLanguageCodes = {
   'ja_easy': 'ja',
-  'zh_Hant': 'zh-HANT'
+  'zh_Hant': 'zh-HANT',
+  'zh': 'zh-Hans'
 }
 
 const internalToBrowserLocale = code => specialLanguageCodes[code] || code
 
+const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
+
+const getLanguageName = (code) => {
+  const specialLanguageNames = {
+    '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 localeService = {
-  internalToBrowserLocale
+  internalToBrowserLocale,
+  internalToBackendLocale,
+  languages,
+  getLanguageName
 }
 
 export default localeService
diff --git a/yarn.lock b/yarn.lock
index a898a932..e413b6d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5722,6 +5722,11 @@ js-base64@^2.1.9:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e"
 
+js-cookie@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
+  integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-- 
cgit v1.2.3-70-g09d2


From 55adcd822e194be8aaeb9d6b649de90e9d5e1e45 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 12 Jun 2022 16:31:56 +0300
Subject: fix animations, replace ugly old mentionlink tooltips with new
 usercard ones

---
 src/App.scss                                       |   2 +-
 src/components/mention_link/mention_link.js        |   8 +-
 src/components/mention_link/mention_link.scss      |   7 +-
 src/components/mention_link/mention_link.vue       | 133 ++++++++++++---------
 src/components/popover/popover.js                  |  40 +++++--
 src/components/popover/popover.vue                 |  36 +++---
 src/components/settings_modal/tabs/general_tab.vue |   2 +-
 src/i18n/en.json                                   |   2 +-
 8 files changed, 140 insertions(+), 90 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/App.scss b/src/App.scss
index 5cd0b96e..79c07683 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -829,7 +829,7 @@ option {
 // Vue transitions
 .fade-enter-active,
 .fade-leave-active {
-  transition: opacity 0.2s;
+  transition: opacity 0.3s;
 }
 
 .fade-enter-from,
diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 55eea195..4b088f88 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
 import { mapGetters, mapState } from 'vuex'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import UserAvatar from '../user_avatar/user_avatar.vue'
+import { defineAsyncComponent } from 'vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
 import {
   faAt
@@ -14,7 +15,9 @@ library.add(
 const MentionLink = {
   name: 'MentionLink',
   components: {
-    UserAvatar
+    UserAvatar,
+    Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
+    UserCard: defineAsyncComponent(() => import('../user_card/user_card.vue'))
   },
   props: {
     url: {
@@ -36,6 +39,7 @@ const MentionLink = {
   },
   methods: {
     onClick () {
+      if (this.shouldShowTooltip) return
       const link = generateProfileLink(
         this.userId || this.user.id,
         this.userScreenName || this.user.screen_name
@@ -110,7 +114,7 @@ const MentionLink = {
       }
     },
     shouldShowTooltip () {
-      return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
+      return this.mergedConfig.mentionLinkShowTooltip
     },
     shouldShowAvatar () {
       return this.mergedConfig.mentionLinkShowAvatar
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index 1d856ff9..3b3a05b1 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -101,7 +101,6 @@
   }
 
   &:hover .new .full {
-    opacity: 1;
     pointer-events: initial;
   }
 
@@ -113,3 +112,9 @@
     color: var(--faint, $fallback--faint);
   }
 }
+
+.mention-link-popover {
+  max-width: 70ch;
+  max-height: 20rem;
+  overflow: hidden;
+}
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 022f04c7..686ad27f 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -9,66 +9,85 @@
       class="original"
       target="_blank"
       v-html="content"
-    /><!-- eslint-enable vue/no-v-html --><span
-      v-if="user"
-      class="new"
-      :style="style"
-      :class="classnames"
+    /><!-- eslint-enable vue/no-v-html -->
+    <Popover
+      trigger="click"
+      :bound-to="{ x: 'container'}"
+      bound-to-selector=".column"
+      popover-class="popover-default mention-popover"
+      :disabled="!shouldShowTooltip"
     >
-      <a
-        class="short button-unstyled"
-        :class="{ '-with-tooltip': shouldShowTooltip }"
-        :href="url"
-        @click.prevent="onClick"
-      >
-        <!-- eslint-disable vue/no-v-html -->
-        <UserAvatar
-          v-if="shouldShowAvatar"
-          class="mention-avatar"
-          :user="user"
-        /><span
-          class="shortName"
-        ><FAIcon
-          v-if="useAtIcon"
-          size="sm"
-          icon="at"
-          class="at"
-        />{{ !useAtIcon ? '@' : '' }}<span
-          class="userName"
-          v-html="userName"
-        /><span
-          v-if="shouldShowFullUserName"
-          class="serverName"
-          :class="{ '-faded': shouldFadeDomain }"
-          v-html="'@' + serverName"
-        />
-        </span>
-        <span
-          v-if="isYou && shouldShowYous"
-          :class="{ '-you': shouldBoldenYou }"
-        > {{ ' ' + $t('status.you') }}</span>
-        <!-- eslint-enable vue/no-v-html -->
-      </a><span
-        v-if="shouldShowTooltip"
-        class="full popover-default"
-        :class="[highlightType]"
-      >
+      <template v-slot:trigger>
         <span
-          class="userNameFull"
+          v-if="user"
+          class="new"
+          :style="style"
+          :class="classnames"
         >
-          <!-- eslint-disable vue/no-v-html -->
-          @<span
-            class="userName"
-            v-html="userName"
-          /><span
-            class="serverName"
-            :class="{ '-faded': shouldFadeDomain }"
-            v-html="'@' + serverName"
-          />
-          <!-- eslint-enable vue/no-v-html -->
-        </span>
-      </span>
-    </span>
+          <a
+            class="short button-unstyled"
+            :class="{ '-with-tooltip': shouldShowTooltip }"
+            :href="url"
+            @click.prevent="onClick"
+          >
+            <!-- eslint-disable vue/no-v-html -->
+            <UserAvatar
+              v-if="shouldShowAvatar"
+              class="mention-avatar"
+              :user="user"
+            /><span
+              class="shortName"
+            ><FAIcon
+              v-if="useAtIcon"
+              size="sm"
+              icon="at"
+              class="at"
+            />{{ !useAtIcon ? '@' : '' }}<span
+              class="userName"
+              v-html="userName"
+            /><span
+              v-if="shouldShowFullUserName"
+              class="serverName"
+              :class="{ '-faded': shouldFadeDomain }"
+              v-html="'@' + serverName"
+            />
+            </span>
+            <span
+              v-if="isYou && shouldShowYous"
+              :class="{ '-you': shouldBoldenYou }"
+            > {{ ' ' + $t('status.you') }}</span>
+            <!-- eslint-enable vue/no-v-html -->
+          </a><span
+            v-if="shouldShowTooltip"
+            class="full"
+          >
+            <span
+              class="userNameFull"
+            >
+              <!-- eslint-disable vue/no-v-html -->
+              @<span
+                class="userName"
+                v-html="userName"
+              /><span
+                class="serverName"
+                :class="{ '-faded': shouldFadeDomain }"
+                v-html="'@' + serverName"
+              />
+              <!-- eslint-enable vue/no-v-html -->
+            </span>
+          </span>
+        </span></template>
+      <template v-slot:content>
+        <UserCard
+          class="mention-link-popover"
+          :user-id="user.id"
+          :hide-bio="true"
+          :bordered="false"
+          :allow-zooming-avatar="true"
+          :rounded="true"
+        />
+      </template>
+    </Popover>
   </span>
 </template>
 
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index a91c084f..3c3a95bd 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -31,13 +31,18 @@ const Popover = {
 
     // If true, subtract padding when calculating position for the popover,
     // use it when popover offset looks to be different on top vs bottom.
-    removePadding: Boolean
+    removePadding: Boolean,
+
+    // self-explanatory (i hope)
+    disabled: Boolean
   },
   data () {
     return {
       hidden: true,
-      styles: { opacity: 0 },
-      oldSize: { width: 0, height: 0 }
+      styles: {},
+      oldSize: { width: 0, height: 0 },
+      // used to avoid blinking if hovered onto popover
+      graceTimeout: null
     }
   },
   methods: {
@@ -47,9 +52,7 @@ const Popover = {
     },
     updateStyles () {
       if (this.hidden) {
-        this.styles = {
-          opacity: 0
-        }
+        this.styles = {}
         return
       }
 
@@ -132,7 +135,6 @@ const Popover = {
       // Note, separate translateX and translateY avoids blurry text on chromium,
       // single translate or translate3d resulted in blurry text.
       this.styles = {
-        opacity: 1,
         left: `${Math.round(translateX)}px`,
         top: `${Math.round(translateY)}px`,
         position: 'fixed'
@@ -143,6 +145,7 @@ const Popover = {
       }
     },
     showPopover () {
+      if (this.disabled) return
       const wasHidden = this.hidden
       this.hidden = false
       this.$nextTick(() => {
@@ -153,13 +156,30 @@ const Popover = {
     hidePopover () {
       if (!this.hidden) this.$emit('close')
       this.hidden = true
-      this.styles = { opacity: 0 }
     },
     onMouseenter (e) {
-      if (this.trigger === 'hover') this.showPopover()
+      if (this.trigger === 'hover') {
+        clearTimeout(this.graceTimeout)
+        this.graceTimeout = null
+        this.showPopover()
+      }
     },
     onMouseleave (e) {
-      if (this.trigger === 'hover') this.hidePopover()
+      if (this.trigger === 'hover') {
+        this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
+      }
+    },
+    onMouseenterContent (e) {
+      if (this.trigger === 'hover') {
+        clearTimeout(this.graceTimeout)
+        this.graceTimeout = null
+        this.showPopover()
+      }
+    },
+    onMouseleaveContent (e) {
+      if (this.trigger === 'hover') {
+        this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
+      }
     },
     onClick (e) {
       if (this.trigger === 'click') {
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 6103fbdc..528c4fb2 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -1,5 +1,5 @@
 <template>
-  <div
+  <span
     @mouseenter="onMouseenter"
     @mouseleave="onMouseleave"
   >
@@ -12,21 +12,25 @@
       <slot name="trigger" />
     </button>
     <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>
+      <transition name="fade">
+        <div
+          v-if="!hidden"
+          ref="content"
+          :style="styles"
+          class="popover"
+          :class="popoverClass || 'popover-default'"
+          @mouseenter="onMouseenterContent"
+          @mouseleave="onMouseleaveContent"
+        >
+          <slot
+            name="content"
+            class="popover-inner"
+            :close="hidePopover"
+          />
+        </div>
+      </transition>
     </teleport>
-  </div>
+  </span>
 </template>
 
 <script src="./popover.js" />
@@ -47,8 +51,6 @@
 }
 
 .popover-default {
-  transition: opacity 0.3s;
-
   &:after {
     content: '';
     position: absolute;
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 1fe51b6d..5b4a126e 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -269,7 +269,7 @@
               path="mentionLinkShowTooltip"
               expert="1"
             >
-              {{ $t('settings.mention_link_show_tooltip') }}
+              {{ $t('settings.mention_link_use_tooltip') }}
             </BooleanSetting>
           </li>
         </ul>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0cfda804..053e91ff 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -523,7 +523,7 @@
     "mention_link_display_short": "always as short names (e.g. {'@'}foo)",
     "mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)",
     "mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)",
-    "mention_link_show_tooltip": "Show full user names as tooltip for remote users",
+    "mention_link_use_tooltip": "Show user card when clicking mention links",
     "mention_link_show_avatar": "Show user avatar beside the link",
     "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
     "mention_link_bolden_you": "Highlight mention of you when you are mentioned",
-- 
cgit v1.2.3-70-g09d2


From 9c9dc2053b68813f748bc7cd656987242f45158c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 27 Jun 2022 19:12:16 +0300
Subject: move tooltips setting

---
 src/components/settings_modal/tabs/general_tab.vue | 20 ++++++++------------
 1 file changed, 8 insertions(+), 12 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 5b4a126e..61416394 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -261,18 +261,14 @@
             {{ $t('settings.mention_link_display') }}
           </ChoiceSetting>
         </li>
-        <ul
-          class="setting-list suboptions"
-        >
-          <li v-if="mentionLinkDisplay === 'short'">
-            <BooleanSetting
-              path="mentionLinkShowTooltip"
-              expert="1"
-            >
-              {{ $t('settings.mention_link_use_tooltip') }}
-            </BooleanSetting>
-          </li>
-        </ul>
+        <li>
+          <BooleanSetting
+            path="mentionLinkShowTooltip"
+            expert="1"
+          >
+            {{ $t('settings.mention_link_use_tooltip') }}
+          </BooleanSetting>
+        </li>
         <li>
           <BooleanSetting
             path="useAtIcon"
-- 
cgit v1.2.3-70-g09d2


From d232fa3dc3ff83a09a4ad583a123b419f4d4fa42 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 27 Jun 2022 19:52:40 +0300
Subject: close on avatar click again, add zooming as option

---
 src/components/settings_modal/tabs/general_tab.vue | 5 +++++
 src/components/user_card/user_card.js              | 9 ++++++++-
 src/components/user_card/user_card.vue             | 1 +
 src/components/user_popover/user_popover.js        | 7 ++++++-
 src/components/user_popover/user_popover.vue       | 7 ++++---
 src/i18n/en.json                                   | 1 +
 src/modules/config.js                              | 1 +
 7 files changed, 26 insertions(+), 5 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 61416394..0a300311 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -74,6 +74,11 @@
             {{ $t('settings.show_scrollbars') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="userPopoverZoom">
+            {{ $t('settings.user_popover_avatar_zoom') }}
+          </BooleanSetting>
+        </li>
         <li>
           <ChoiceSetting
             v-if="user"
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 52e8c079..9a8dcb23 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -38,7 +38,8 @@ export default {
     'rounded',
     'bordered',
     'allowZoomingAvatar',
-    'onClose'
+    'onClose',
+    'onAvatarClick'
   ],
   data () {
     return {
@@ -182,6 +183,12 @@ export default {
     },
     mentionUser () {
       this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
+    },
+    onAvatarClickHandler (e) {
+      if (this.onAvatarClick) {
+        e.preventDefault()
+        this.onAvatarClick()
+      }
     }
   }
 }
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 936d6798..62d4a94d 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -30,6 +30,7 @@
           <router-link
             v-else
             :to="userProfileLink(user)"
+            @click="onAvatarClickHandler"
           >
             <UserAvatar
               :better-shadow="betterShadow"
diff --git a/src/components/user_popover/user_popover.js b/src/components/user_popover/user_popover.js
index 3b3722ce..fa7f9781 100644
--- a/src/components/user_popover/user_popover.js
+++ b/src/components/user_popover/user_popover.js
@@ -4,11 +4,16 @@ import { defineAsyncComponent } from 'vue'
 const UserPopover = {
   name: 'UserPopover',
   props: [
-    'userId', 'overlayCenters', 'disabled', 'overlay-centers-selector'
+    'userId', 'overlayCenters', 'disabled', 'overlayCentersSelector'
   ],
   components: {
     UserCard,
     Popover: defineAsyncComponent(() => import('../popover/popover.vue'))
+  },
+  computed: {
+    userPopoverZoom () {
+      return this.$store.getters.mergedConfig.userPopoverZoom
+    }
   }
 }
 
diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue
index f4f505ae..c14cea4d 100644
--- a/src/components/user_popover/user_popover.vue
+++ b/src/components/user_popover/user_popover.vue
@@ -2,7 +2,7 @@
 <Popover
   trigger="click"
   popover-class="popover-default user-popover"
-  :overlay-centers-selector="overlayCentersSelector || '.user-info-avatar-link .Avatar'"
+  :overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
   :overlay-centers="overlayCenters"
   :disabled="disabled"
 >
@@ -14,8 +14,9 @@
       class="user-popover"
       :user-id="userId"
       :hide-bio="true"
-      :allow-zooming-avatar="true"
-      :onClose="close"
+      :allow-zooming-avatar="userPopoverZoom"
+      :on-avatar-click="userPopoverZoom ? null : close"
+      :on-close="close"
     />
   </template>
 </Popover>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 5f3f1334..9a8e54d6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -550,6 +550,7 @@
     "mention_link_show_avatar": "Show user avatar beside the link",
     "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
     "mention_link_bolden_you": "Highlight mention of you when you are mentioned",
+    "user_popover_avatar_zoom": "Clicking on user avatar in popover zooms it instead of closing the popover",
     "fun": "Fun",
     "greentext": "Meme arrows",
     "show_yous": "Show (You)s",
diff --git a/src/modules/config.js b/src/modules/config.js
index 6ae2e754..14add385 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -81,6 +81,7 @@ export const defaultState = {
   useContainFit: true,
   disableStickyHeaders: false,
   showScrollbars: false,
+  userPopoverZoom: false,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
   mentionLinkDisplay: undefined, // instance default
-- 
cgit v1.2.3-70-g09d2


From 2c3c2bb5b032f534a7ff9bc7be4687373baa2f8d Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 19 Jul 2022 19:33:48 +0300
Subject: fix avatar not closing, add option to put popovers next to avatar
 instead of over it

---
 src/components/settings_modal/tabs/general_tab.vue |  5 +++++
 src/components/user_card/user_card.js              |  5 ++---
 src/components/user_card/user_card.scss            | 12 ++++++++----
 src/components/user_card/user_card.vue             | 14 ++++++++++----
 src/components/user_popover/user_popover.js        |  3 +++
 src/components/user_popover/user_popover.vue       |  5 ++---
 src/components/user_profile/user_profile.vue       |  2 +-
 src/i18n/en.json                                   |  1 +
 src/modules/config.js                              |  1 +
 9 files changed, 33 insertions(+), 15 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 0a300311..abb0ed40 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -79,6 +79,11 @@
             {{ $t('settings.user_popover_avatar_zoom') }}
           </BooleanSetting>
         </li>
+        <li>
+          <BooleanSetting path="userPopoverOverlay">
+            {{ $t('settings.user_popover_avatar_overlay') }}
+          </BooleanSetting>
+        </li>
         <li>
           <ChoiceSetting
             v-if="user"
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 9a8dcb23..b0fa11b3 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -37,9 +37,8 @@ export default {
     'hideBio',
     'rounded',
     'bordered',
-    'allowZoomingAvatar',
-    'onClose',
-    'onAvatarClick'
+    'avatarAction', // default - open profile, 'zoom' - zoom, function - call function
+    'onClose'
   ],
   data () {
     return {
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
index 65299e60..a0bbc6a6 100644
--- a/src/components/user_card/user_card.scss
+++ b/src/components/user_card/user_card.scss
@@ -133,23 +133,27 @@
       min-width: 0;
     }
 
+    > a {
+      vertical-align: middle;
+      display: flex;
+    }
+
     .Avatar {
       --_avatarShadowBox: var(--avatarShadow);
       --_avatarShadowFilter: var(--avatarShadowFilter);
       --_avatarShadowInset: var(--avatarShadowInset);
 
-      flex: 1 0 100%;
       width: 56px;
       height: 56px;
       object-fit: cover;
     }
   }
 
-  &-avatar-link {
+  &-avatar {
     position: relative;
     cursor: pointer;
 
-    &-overlay {
+    &.-overlay {
       position: absolute;
       left: 0;
       top: 0;
@@ -169,7 +173,7 @@
       }
     }
 
-    &:hover &-overlay {
+    &:hover &.-overlay {
       opacity: 1;
     }
   }
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 62d4a94d..bc23e68e 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -12,25 +12,31 @@
       <div class="user-info">
         <div class="container">
           <a
-            v-if="allowZoomingAvatar"
-            class="user-info-avatar-link"
+            v-if="avatarAction === 'zoom'"
+            class="user-info-avatar -link"
             @click="zoomAvatar"
           >
             <UserAvatar
               :better-shadow="betterShadow"
               :user="user"
             />
-            <div class="user-info-avatar-link-overlay">
+            <div class="user-info-avatar -link -overlay">
               <FAIcon
                 class="fa-scale-110 fa-old-padding"
                 icon="search-plus"
               />
             </div>
           </a>
+          <UserAvatar
+            v-else-if="typeof avatarAction === 'function'"
+            @click="avatarAction"
+            class="user-info-avatar"
+            :better-shadow="betterShadow"
+            :user="user"
+          />
           <router-link
             v-else
             :to="userProfileLink(user)"
-            @click="onAvatarClickHandler"
           >
             <UserAvatar
               :better-shadow="betterShadow"
diff --git a/src/components/user_popover/user_popover.js b/src/components/user_popover/user_popover.js
index fa7f9781..69b25383 100644
--- a/src/components/user_popover/user_popover.js
+++ b/src/components/user_popover/user_popover.js
@@ -13,6 +13,9 @@ const UserPopover = {
   computed: {
     userPopoverZoom () {
       return this.$store.getters.mergedConfig.userPopoverZoom
+    },
+    userPopoverOverlay () {
+      return this.$store.getters.mergedConfig.userPopoverOverlay
     }
   }
 }
diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue
index c14cea4d..72bb7f77 100644
--- a/src/components/user_popover/user_popover.vue
+++ b/src/components/user_popover/user_popover.vue
@@ -3,7 +3,7 @@
   trigger="click"
   popover-class="popover-default user-popover"
   :overlay-centers-selector="overlayCentersSelector || '.user-info .Avatar'"
-  :overlay-centers="overlayCenters"
+  :overlay-centers="overlayCenters && userPopoverOverlay"
   :disabled="disabled"
 >
   <template v-slot:trigger>
@@ -14,8 +14,7 @@
       class="user-popover"
       :user-id="userId"
       :hide-bio="true"
-      :allow-zooming-avatar="userPopoverZoom"
-      :on-avatar-click="userPopoverZoom ? null : close"
+      :avatar-action="userPopoverZoom ? 'zoom' : close"
       :on-close="close"
     />
   </template>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 62792599..430d0915 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -8,7 +8,7 @@
         :user-id="userId"
         :switcher="true"
         :selected="timeline.viewing"
-        :allow-zooming-avatar="true"
+        :avatar-action="zoom"
         rounded="top"
       />
       <div
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 9a8e54d6..fe02664f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -551,6 +551,7 @@
     "mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
     "mention_link_bolden_you": "Highlight mention of you when you are mentioned",
     "user_popover_avatar_zoom": "Clicking on user avatar in popover zooms it instead of closing the popover",
+    "user_popover_avatar_overlay": "Show user popover over user avatar",
     "fun": "Fun",
     "greentext": "Meme arrows",
     "show_yous": "Show (You)s",
diff --git a/src/modules/config.js b/src/modules/config.js
index 14add385..f7f142ad 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -82,6 +82,7 @@ export const defaultState = {
   disableStickyHeaders: false,
   showScrollbars: false,
   userPopoverZoom: false,
+  userPopoverOverlay: true,
   greentext: undefined, // instance default
   useAtIcon: undefined, // instance default
   mentionLinkDisplay: undefined, // instance default
-- 
cgit v1.2.3-70-g09d2


From 3d9b3a7352acf991ae11524902bee50a60af79c6 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 27 Jul 2022 20:25:30 +0300
Subject: make user popover options expert

---
 src/components/settings_modal/tabs/general_tab.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/components/settings_modal/tabs/general_tab.vue')

diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index abb0ed40..e201ab9d 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -75,12 +75,12 @@
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="userPopoverZoom">
+          <BooleanSetting path="userPopoverZoom" expert="1">
             {{ $t('settings.user_popover_avatar_zoom') }}
           </BooleanSetting>
         </li>
         <li>
-          <BooleanSetting path="userPopoverOverlay">
+          <BooleanSetting path="userPopoverOverlay" expert="1">
             {{ $t('settings.user_popover_avatar_overlay') }}
           </BooleanSetting>
         </li>
-- 
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/components/settings_modal/tabs/general_tab.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

