From 7a013ac39392ef251c0789f27dd4660dcd30bd6d Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Wed, 15 Jan 2020 20:22:54 +0000
Subject: Implement domain mutes v2

---
 src/components/user_settings/user_settings.vue | 157 ++++++++++++++++++-------
 1 file changed, 112 insertions(+), 45 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 3f1982a6..2222c293 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -509,59 +509,114 @@
         </div>
 
         <div :label="$t('settings.mutes_tab')">
-          <div class="profile-edit-usersearch-wrapper">
-            <Autosuggest
-              :filter="filterUnMutedUsers"
-              :query="queryUserIds"
-              :placeholder="$t('settings.search_user_to_mute')"
-            >
-              <MuteCard
-                slot-scope="row"
-                :user-id="row.item"
-              />
-            </Autosuggest>
-          </div>
-          <MuteList
-            :refresh="true"
-            :get-key="identity"
-          >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-            >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => muteUsers(selected)"
+          <tab-switcher>
+            <div label="Users">
+              <div class="profile-edit-usersearch-wrapper">
+                <Autosuggest
+                  :filter="filterUnMutedUsers"
+                  :query="queryUserIds"
+                  :placeholder="$t('settings.search_user_to_mute')"
+                >
+                  <MuteCard
+                    slot-scope="row"
+                    :user-id="row.item"
+                  />
+                </Autosuggest>
+              </div>
+              <MuteList
+                :refresh="true"
+                :get-key="identity"
+              >
+                <template
+                  slot="header"
+                  slot-scope="{selected}"
+                >
+                  <div class="profile-edit-bulk-actions">
+                    <ProgressButton
+                      v-if="selected.length > 0"
+                      class="btn btn-default"
+                      :click="() => muteUsers(selected)"
+                    >
+                      {{ $t('user_card.mute') }}
+                      <template slot="progress">
+                        {{ $t('user_card.mute_progress') }}
+                      </template>
+                    </ProgressButton>
+                    <ProgressButton
+                      v-if="selected.length > 0"
+                      class="btn btn-default"
+                      :click="() => unmuteUsers(selected)"
+                    >
+                      {{ $t('user_card.unmute') }}
+                      <template slot="progress">
+                        {{ $t('user_card.unmute_progress') }}
+                      </template>
+                    </ProgressButton>
+                  </div>
+                </template>
+                <template
+                  slot="item"
+                  slot-scope="{item}"
+                >
+                  <MuteCard :user-id="item" />
+                </template>
+                <template slot="empty">
+                  {{ $t('settings.no_mutes') }}
+                </template>
+              </MuteList>
+            </div>
+
+            <div :label="$t('settings.domain_mutes')">
+              <div class="profile-edit-domain-mute-form">
+                <input
+                  v-model="newDomainToMute"
+                  :placeholder="$t('settings.type_domains_to_mute')"
+                  type="text"
+                  @keyup.enter="muteDomain"
                 >
-                  {{ $t('user_card.mute') }}
-                  <template slot="progress">
-                    {{ $t('user_card.mute_progress') }}
-                  </template>
-                </ProgressButton>
                 <ProgressButton
-                  v-if="selected.length > 0"
                   class="btn btn-default"
-                  :click="() => unmuteUsers(selected)"
+                  :click="muteDomain"
                 >
-                  {{ $t('user_card.unmute') }}
+                  {{ $t('domain_mute_card.mute') }}
                   <template slot="progress">
-                    {{ $t('user_card.unmute_progress') }}
+                    {{ $t('domain_mute_card.mute_progress') }}
                   </template>
                 </ProgressButton>
               </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-            >
-              <MuteCard :user-id="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_mutes') }}
-            </template>
-          </MuteList>
+              <DomainMuteList
+                :refresh="true"
+                :get-key="identity"
+              >
+                <template
+                  slot="header"
+                  slot-scope="{selected}"
+                >
+                  <div class="profile-edit-bulk-actions">
+                    <ProgressButton
+                      v-if="selected.length > 0"
+                      class="btn btn-default"
+                      :click="() => unmuteDomains(selected)"
+                    >
+                      {{ $t('domain_mute_card.unmute') }}
+                      <template slot="progress">
+                        {{ $t('domain_mute_card.unmute_progress') }}
+                      </template>
+                    </ProgressButton>
+                  </div>
+                </template>
+                <template
+                  slot="item"
+                  slot-scope="{item}"
+                >
+                  <DomainMuteCard :domain="item" />
+                </template>
+                <template slot="empty">
+                  {{ $t('settings.no_mutes') }}
+                </template>
+              </DomainMuteList>
+            </div>
+          </tab-switcher>
         </div>
       </tab-switcher>
     </div>
@@ -639,6 +694,18 @@
     }
   }
 
+  &-domain-mute-form {
+    padding: 1em;
+    display: flex;
+    flex-direction: column;
+
+    button {
+      align-self: flex-end;
+      margin-top: 1em;
+      width: 10em;
+    }
+  }
+
   .setting-subitem {
     margin-left: 1.75em;
   }
-- 
cgit v1.2.3-70-g09d2


From 9bbf10b55d97f6dbe3197ebbd1bb29d294ff6b55 Mon Sep 17 00:00:00 2001
From: kPherox <admin@mail.kr-kp.com>
Date: Tue, 4 Feb 2020 04:26:32 +0900
Subject: Add setting for allow_following_move

---
 src/components/user_settings/user_settings.js               | 2 ++
 src/components/user_settings/user_settings.vue              | 9 ++++++---
 src/services/entity_normalizer/entity_normalizer.service.js | 2 ++
 3 files changed, 10 insertions(+), 3 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 38373056..eca6f9b1 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -55,6 +55,7 @@ const UserSettings = {
       showRole: this.$store.state.users.currentUser.show_role,
       role: this.$store.state.users.currentUser.role,
       discoverable: this.$store.state.users.currentUser.discoverable,
+      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
       pickAvatarBtnVisible: true,
       bannerUploading: false,
       backgroundUploading: false,
@@ -162,6 +163,7 @@ const UserSettings = {
             hide_follows: this.hideFollows,
             hide_followers: this.hideFollowers,
             discoverable: this.discoverable,
+            allow_following_move: this.allowFollowingMove,
             hide_follows_count: this.hideFollowsCount,
             hide_followers_count: this.hideFollowersCount,
             show_role: this.showRole
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 2222c293..8b2336b4 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -90,9 +90,7 @@
               </Checkbox>
             </p>
             <p>
-              <Checkbox
-                v-model="hideFollowers"
-              >
+              <Checkbox v-model="hideFollowers">
                 {{ $t('settings.hide_followers_description') }}
               </Checkbox>
             </p>
@@ -104,6 +102,11 @@
                 {{ $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'">
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index a3d0b782..3bc46886 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -81,6 +81,8 @@ export const parseUser = (data) => {
         output.subscribed = relationship.subscribing
       }
 
+      output.allow_following_move = data.pleroma.allow_following_move
+
       output.hide_follows = data.pleroma.hide_follows
       output.hide_followers = data.pleroma.hide_followers
       output.hide_follows_count = data.pleroma.hide_follows_count
-- 
cgit v1.2.3-70-g09d2


From 2e35289c3376881ca17b9330113c816a3327f245 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 3 May 2020 17:36:12 +0300
Subject: initial work on settings modal

---
 src/App.js                                         |   5 +
 src/App.scss                                       |   3 +
 src/App.vue                                        |   7 +-
 .../mobile_post_status_button.js                   |   2 +-
 .../post_status_modal/post_status_modal.js         |   3 -
 .../post_status_modal/post_status_modal.vue        |   1 -
 src/components/settings_modal/settings_modal.js    |  39 ++
 src/components/settings_modal/settings_modal.scss  |  59 ++
 src/components/settings_modal/settings_modal.vue   |  31 ++
 .../settings_modal/tabs/data_import_export.js      |  65 +++
 .../settings_modal/tabs/data_import_export.vue     |  43 ++
 .../settings_modal/tabs/mutes_and_blocks.js        | 124 +++++
 .../settings_modal/tabs/mutes_and_blocks.vue       | 173 ++++++
 .../settings_modal/tabs/notifications.js           |  27 +
 .../settings_modal/tabs/notifications.vue          |  42 ++
 src/components/settings_modal/tabs/profile.js      | 179 ++++++
 src/components/settings_modal/tabs/profile.scss    |  82 +++
 src/components/settings_modal/tabs/profile.vue     | 213 ++++++++
 src/components/settings_modal/tabs/security.js     | 106 ++++
 src/components/settings_modal/tabs/security.vue    | 143 +++++
 src/components/tab_switcher/tab_switcher.js        |   7 +-
 src/components/tab_switcher/tab_switcher.scss      | 220 +++++---
 src/components/user_settings/user_settings.js      | 257 +--------
 src/components/user_settings/user_settings.vue     | 597 ---------------------
 src/i18n/en.json                                   |   1 +
 src/modules/interface.js                           |  28 +
 26 files changed, 1530 insertions(+), 927 deletions(-)
 create mode 100644 src/components/settings_modal/settings_modal.js
 create mode 100644 src/components/settings_modal/settings_modal.scss
 create mode 100644 src/components/settings_modal/settings_modal.vue
 create mode 100644 src/components/settings_modal/tabs/data_import_export.js
 create mode 100644 src/components/settings_modal/tabs/data_import_export.vue
 create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.js
 create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.vue
 create mode 100644 src/components/settings_modal/tabs/notifications.js
 create mode 100644 src/components/settings_modal/tabs/notifications.vue
 create mode 100644 src/components/settings_modal/tabs/profile.js
 create mode 100644 src/components/settings_modal/tabs/profile.scss
 create mode 100644 src/components/settings_modal/tabs/profile.vue
 create mode 100644 src/components/settings_modal/tabs/security.js
 create mode 100644 src/components/settings_modal/tabs/security.vue

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

diff --git a/src/App.js b/src/App.js
index 61b5eec1..4d9d50d4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
 import FeaturesPanel from './components/features_panel/features_panel.vue'
 import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
 import ChatPanel from './components/chat_panel/chat_panel.vue'
+import SettingsModal from './components/settings_modal/settings_modal.vue'
 import MediaModal from './components/media_modal/media_modal.vue'
 import SideDrawer from './components/side_drawer/side_drawer.vue'
 import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@@ -29,6 +30,7 @@ export default {
     SideDrawer,
     MobilePostStatusButton,
     MobileNav,
+    SettingsModal,
     UserReportingModal,
     PostStatusModal
   },
@@ -112,6 +114,9 @@ export default {
     onSearchBarToggled (hidden) {
       this.searchBarHidden = hidden
     },
+    openSettingsModal () {
+      this.$store.dispatch('openSettingsModal')
+    },
     updateMobileState () {
       const mobileLayout = windowWidth() <= 800
       const changed = mobileLayout !== this.isMobileLayout
diff --git a/src/App.scss b/src/App.scss
index 89aa3215..7db9461c 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -860,6 +860,7 @@ nav {
   }
 }
 
+// DELETE
 .setting-item {
   border-bottom: 2px solid var(--fg, $fallback--fg);
   margin: 1em 1em 1.4em;
@@ -905,6 +906,8 @@ nav {
     max-width: 6em;
   }
 }
+// DELETE
+
 .select-multiple {
   display: flex;
   .option-list {
diff --git a/src/App.vue b/src/App.vue
index ff62fc51..db3f981f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -46,15 +46,15 @@
             @toggled="onSearchBarToggled"
             @click.stop.native
           />
-          <router-link
+          <a
             class="mobile-hidden"
-            :to="{ name: 'settings'}"
+            @click.stop="openSettingsModal"
           >
             <i
               class="button-icon icon-cog nav-icon"
               :title="$t('nav.preferences')"
             />
-          </router-link>
+          </a>
           <a
             v-if="currentUser && currentUser.role === 'admin'"
             href="/pleroma/admin/#/login-pleroma"
@@ -122,6 +122,7 @@
     <MobilePostStatusButton />
     <UserReportingModal />
     <PostStatusModal />
+    <SettingsModal />
     <portal-target name="modal" />
   </div>
 </template>
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index 0ad12bb1..ff2d4eaa 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -52,7 +52,7 @@ const MobilePostStatusButton = {
       window.removeEventListener('scroll', this.handleScrollEnd)
     },
     openPostForm () {
-      this.$store.dispatch('openPostStatusModal')
+      this.$store.dispatch('openSettingsModal')
     },
     handleOSK () {
       // This is a big hack: we're guessing from changed window sizes if the
diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js
index b44354db..be945400 100644
--- a/src/components/post_status_modal/post_status_modal.js
+++ b/src/components/post_status_modal/post_status_modal.js
@@ -13,9 +13,6 @@ const PostStatusModal = {
     }
   },
   computed: {
-    isLoggedIn () {
-      return !!this.$store.state.users.currentUser
-    },
     modalActivated () {
       return this.$store.state.postStatus.modalActivated
     },
diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue
index dbcd321e..07c58f74 100644
--- a/src/components/post_status_modal/post_status_modal.vue
+++ b/src/components/post_status_modal/post_status_modal.vue
@@ -1,6 +1,5 @@
 <template>
   <Modal
-    v-if="isLoggedIn && !resettingForm"
     :is-open="modalActivated"
     class="post-form-modal-view"
     @backdropClicked="closeModal"
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
new file mode 100644
index 00000000..1f4c038f
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.js
@@ -0,0 +1,39 @@
+import Modal from '../modal/modal.vue'
+import TabSwitcher from '../tab_switcher/tab_switcher.js'
+
+import Profile from './tabs/profile.vue'
+import Security from './tabs/security.vue'
+import Notifications from './tabs/notifications.vue'
+import DataImportExport from './tabs/data_import_export.vue'
+import MutesAndBlocks from './tabs/mutes_and_blocks.vue'
+
+const SettingsModal = {
+  components: {
+    Modal,
+    TabSwitcher,
+    Profile,
+    Security,
+    Notifications,
+    DataImportExport,
+    MutesAndBlocks
+  },
+  data () {
+    return {
+      resettingForm: false
+    }
+  },
+  computed: {
+    isLoggedIn () {
+      return !!this.$store.state.users.currentUser
+    },
+    modalActivated () {
+      return this.$store.state.interface.settingsModalState !== 'hidden'
+    }
+  },
+  watch: {
+  },
+  methods: {
+  }
+}
+
+export default SettingsModal
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
new file mode 100644
index 00000000..8cea52d2
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.scss
@@ -0,0 +1,59 @@
+@import '../../_variables.scss';
+.settings-modal {
+  .settings_tab-switcher {
+    height: 100%;
+  }
+  .settings-modal-panel {
+    width: 1000px;
+    max-width: 90vw;
+    height: 90vh;
+  }
+  .panel-body {
+    overflow-y: hidden;
+  }
+  .setting-item {
+    border-bottom: 2px solid var(--fg, $fallback--fg);
+    margin: 1em 1em 1.4em;
+    padding-bottom: 1.4em;
+
+    > div {
+      margin-bottom: .5em;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    &:last-child {
+      border-bottom: none;
+      padding-bottom: 0;
+      margin-bottom: 1em;
+    }
+
+    select {
+      min-width: 10em;
+    }
+
+
+    textarea {
+      width: 100%;
+      max-width: 100%;
+      height: 100px;
+    }
+
+    .unavailable,
+    .unavailable i {
+      color: var(--cRed, $fallback--cRed);
+      color: $fallback--cRed;
+    }
+
+    .btn {
+      min-height: 28px;
+      min-width: 10em;
+      padding: 0 2em;
+    }
+
+    .number-input {
+      max-width: 6em;
+    }
+  }
+}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
new file mode 100644
index 00000000..9e35d3f6
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.vue
@@ -0,0 +1,31 @@
+<template>
+<Modal
+  v-if="isLoggedIn && !resettingForm"
+  :is-open="modalActivated"
+  class="settings-modal"
+  >
+  <div class="settings-modal-panel panel">
+    <div class="panel-heading">
+      {{ $t('settings.settings') }}
+    </div>
+    <div class="panel-body">
+      <tab-switcher
+        class="settings_tab-switcher"
+        :sideTabBar="true"
+        :scrollableTabs="true"
+        ref="tabSwitcher"
+        >
+        <div :label="$t('settings.profile_tab')"><Profile /></div>
+        <div :label="$t('settings.security_tab')"><Security /></div>
+        <div :label="$t('settings.notifications')"><Notifications /></div>
+        <div :label="$t('settings.data_import_export_tab')"><DataImportExport /></div>
+        <div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocks /></div>
+      </tab-switcher>
+    </div>
+  </div>
+</Modal>
+</template>
+
+<script src="./settings_modal.js"></script>
+
+<style src="./settings_modal.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js
new file mode 100644
index 00000000..f68d12e9
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export.js
@@ -0,0 +1,65 @@
+import Importer from '../../importer/importer.vue'
+import Exporter from '../../exporter/exporter.vue'
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const DataImportExport = {
+  data () {
+    return {
+      activeTab: 'profile',
+      newDomainToMute: ''
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    Importer,
+    Exporter,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
+  methods: {
+    getFollowsContent () {
+      return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+        .then(this.generateExportableUsersContent)
+    },
+    getBlocksContent () {
+      return this.$store.state.api.backendInteractor.fetchBlocks()
+        .then(this.generateExportableUsersContent)
+    },
+    importFollows (file) {
+      return this.$store.state.api.backendInteractor.importFollows({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    importBlocks (file) {
+      return this.$store.state.api.backendInteractor.importBlocks({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    generateExportableUsersContent (users) {
+      // Get addresses
+      return users.map((user) => {
+        // check is it's a local user
+        if (user && user.is_local) {
+          // append the instance address
+          // eslint-disable-next-line no-undef
+          return user.screen_name + '@' + location.hostname
+        }
+        return user.screen_name
+      }).join('\n')
+    }
+  }
+}
+
+export default DataImportExport
diff --git a/src/components/settings_modal/tabs/data_import_export.vue b/src/components/settings_modal/tabs/data_import_export.vue
new file mode 100644
index 00000000..464df6d3
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export.vue
@@ -0,0 +1,43 @@
+<template>
+<div
+  :label="$t('settings.data_import_export_tab')"
+  >
+  <div class="setting-item">
+    <h2>{{ $t('settings.follow_import') }}</h2>
+    <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
+    <Importer
+      :submit-handler="importFollows"
+      :success-message="$t('settings.follows_imported')"
+      :error-message="$t('settings.follow_import_error')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.follow_export') }}</h2>
+    <Exporter
+      :get-content="getFollowsContent"
+      filename="friends.csv"
+      :export-button-label="$t('settings.follow_export_button')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.block_import') }}</h2>
+    <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
+    <Importer
+      :submit-handler="importBlocks"
+      :success-message="$t('settings.blocks_imported')"
+      :error-message="$t('settings.block_import_error')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.block_export') }}</h2>
+    <Exporter
+      :get-content="getBlocksContent"
+      filename="blocks.csv"
+      :export-button-label="$t('settings.block_export_button')"
+      />
+  </div>
+</div>
+</template>
+
+<script src="./data_import_export.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js
new file mode 100644
index 00000000..51895ddc
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks.js
@@ -0,0 +1,124 @@
+import get from 'lodash/get'
+import map from 'lodash/map'
+import reject from 'lodash/reject'
+import Autosuggest from '../../autosuggest/autosuggest.vue'
+import TabSwitcher from '../../tab_switcher/tab_switcher.js'
+import BlockCard from '../../block_card/block_card.vue'
+import MuteCard from '../../mute_card/mute_card.vue'
+import DomainMuteCard from '../../domain_mute_card/domain_mute_card.vue'
+import SelectableList from '../../selectable_list/selectable_list.vue'
+import ProgressButton from '../../progress_button/progress_button.vue'
+import withSubscription from '../../../hocs/with_subscription/with_subscription'
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const BlockList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
+  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const MuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const DomainMuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const MutesAndBlocks = {
+  data () {
+    return {
+      activeTab: 'profile',
+      newDomainToMute: ''
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    TabSwitcher,
+    BlockList,
+    MuteList,
+    DomainMuteList,
+    BlockCard,
+    MuteCard,
+    DomainMuteCard,
+    ProgressButton,
+    Autosuggest,
+    Checkbox
+  },
+  methods: {
+    importFollows (file) {
+      return this.$store.state.api.backendInteractor.importFollows({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    importBlocks (file) {
+      return this.$store.state.api.backendInteractor.importBlocks({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    generateExportableUsersContent (users) {
+      // Get addresses
+      return users.map((user) => {
+        // check is it's a local user
+        if (user && user.is_local) {
+          // append the instance address
+          // eslint-disable-next-line no-undef
+          return user.screen_name + '@' + location.hostname
+        }
+        return user.screen_name
+      }).join('\n')
+    },
+    activateTab (tabName) {
+      this.activeTab = tabName
+    },
+    filterUnblockedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const user = this.$store.getters.findUser(userId)
+        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
+      })
+    },
+    filterUnMutedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const user = this.$store.getters.findUser(userId)
+        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
+      })
+    },
+    queryUserIds (query) {
+      return this.$store.dispatch('searchUsers', query)
+        .then((users) => map(users, 'id'))
+    },
+    blockUsers (ids) {
+      return this.$store.dispatch('blockUsers', ids)
+    },
+    unblockUsers (ids) {
+      return this.$store.dispatch('unblockUsers', ids)
+    },
+    muteUsers (ids) {
+      return this.$store.dispatch('muteUsers', ids)
+    },
+    unmuteUsers (ids) {
+      return this.$store.dispatch('unmuteUsers', ids)
+    },
+    unmuteDomains (domains) {
+      return this.$store.dispatch('unmuteDomains', domains)
+    },
+    muteDomain () {
+      return this.$store.dispatch('muteDomain', this.newDomainToMute)
+        .then(() => { this.newDomainToMute = '' })
+    }
+  }
+}
+
+export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.vue b/src/components/settings_modal/tabs/mutes_and_blocks.vue
new file mode 100644
index 00000000..3aff47a0
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks.vue
@@ -0,0 +1,173 @@
+<template>
+  <tab-switcher>
+    <div :label="$t('settings.blocks_tab')">
+      <div class="profile-edit-usersearch-wrapper">
+        <Autosuggest
+          :filter="filterUnblockedUsers"
+          :query="queryUserIds"
+          :placeholder="$t('settings.search_user_to_block')"
+          >
+          <BlockCard
+            slot-scope="row"
+            :user-id="row.item"
+            />
+        </Autosuggest>
+      </div>
+      <BlockList
+        :refresh="true"
+        :get-key="identity"
+        >
+        <template
+          slot="header"
+          slot-scope="{selected}"
+          >
+          <div class="profile-edit-bulk-actions">
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default"
+              :click="() => blockUsers(selected)"
+              >
+              {{ $t('user_card.block') }}
+              <template slot="progress">
+                {{ $t('user_card.block_progress') }}
+              </template>
+            </ProgressButton>
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default"
+              :click="() => unblockUsers(selected)"
+              >
+              {{ $t('user_card.unblock') }}
+              <template slot="progress">
+                {{ $t('user_card.unblock_progress') }}
+              </template>
+            </ProgressButton>
+          </div>
+        </template>
+        <template
+          slot="item"
+          slot-scope="{item}"
+          >
+          <BlockCard :user-id="item" />
+        </template>
+        <template slot="empty">
+          {{ $t('settings.no_blocks') }}
+        </template>
+      </BlockList>
+    </div>
+
+    <div :label="$t('settings.mutes_tab')">
+      <tab-switcher>
+        <div label="Users">
+          <div class="profile-edit-usersearch-wrapper">
+            <Autosuggest
+              :filter="filterUnMutedUsers"
+              :query="queryUserIds"
+              :placeholder="$t('settings.search_user_to_mute')"
+              >
+              <MuteCard
+                slot-scope="row"
+                :user-id="row.item"
+                />
+            </Autosuggest>
+          </div>
+          <MuteList
+            :refresh="true"
+            :get-key="identity"
+            >
+            <template
+              slot="header"
+              slot-scope="{selected}"
+              >
+              <div class="profile-edit-bulk-actions">
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => muteUsers(selected)"
+                  >
+                  {{ $t('user_card.mute') }}
+                  <template slot="progress">
+                    {{ $t('user_card.mute_progress') }}
+                  </template>
+                </ProgressButton>
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => unmuteUsers(selected)"
+                  >
+                  {{ $t('user_card.unmute') }}
+                  <template slot="progress">
+                    {{ $t('user_card.unmute_progress') }}
+                  </template>
+                </ProgressButton>
+              </div>
+            </template>
+            <template
+              slot="item"
+              slot-scope="{item}"
+              >
+              <MuteCard :user-id="item" />
+            </template>
+            <template slot="empty">
+              {{ $t('settings.no_mutes') }}
+            </template>
+          </MuteList>
+        </div>
+
+        <div :label="$t('settings.domain_mutes')">
+          <div class="profile-edit-domain-mute-form">
+            <input
+              v-model="newDomainToMute"
+              :placeholder="$t('settings.type_domains_to_mute')"
+              type="text"
+              @keyup.enter="muteDomain"
+              >
+            <ProgressButton
+              class="btn btn-default"
+              :click="muteDomain"
+              >
+              {{ $t('domain_mute_card.mute') }}
+              <template slot="progress">
+                {{ $t('domain_mute_card.mute_progress') }}
+              </template>
+            </ProgressButton>
+          </div>
+          <DomainMuteList
+            :refresh="true"
+            :get-key="identity"
+            >
+            <template
+              slot="header"
+              slot-scope="{selected}"
+              >
+              <div class="profile-edit-bulk-actions">
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => unmuteDomains(selected)"
+                  >
+                  {{ $t('domain_mute_card.unmute') }}
+                  <template slot="progress">
+                    {{ $t('domain_mute_card.unmute_progress') }}
+                  </template>
+                </ProgressButton>
+              </div>
+            </template>
+            <template
+              slot="item"
+              slot-scope="{item}"
+              >
+              <DomainMuteCard :domain="item" />
+            </template>
+            <template slot="empty">
+              {{ $t('settings.no_mutes') }}
+            </template>
+          </DomainMuteList>
+        </div>
+      </tab-switcher>
+    </div>
+  </tab-switcher>
+</template>
+
+<script src="./mutes_and_blocks.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js
new file mode 100644
index 00000000..0a870b3f
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications.js
@@ -0,0 +1,27 @@
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const Notifications = {
+  data () {
+    return {
+      activeTab: 'profile',
+      notificationSettings: this.$store.state.users.currentUser.notification_settings,
+      newDomainToMute: ''
+    }
+  },
+  components: {
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
+  methods: {
+    updateNotificationSettings () {
+      this.$store.state.api.backendInteractor
+        .updateNotificationSettings({ settings: this.notificationSettings })
+    }
+  }
+}
+
+export default Notifications
diff --git a/src/components/settings_modal/tabs/notifications.vue b/src/components/settings_modal/tabs/notifications.vue
new file mode 100644
index 00000000..f9a7c17b
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications.vue
@@ -0,0 +1,42 @@
+<template>
+<div :label="$t('settings.notifications')">
+  <div class="setting-item">
+    <div class="select-multiple">
+      <span class="label">{{ $t('settings.notification_setting') }}</span>
+      <ul class="option-list">
+        <li>
+          <Checkbox v-model="notificationSettings.follows">
+            {{ $t('settings.notification_setting_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.followers">
+            {{ $t('settings.notification_setting_followers') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.non_follows">
+            {{ $t('settings.notification_setting_non_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.non_followers">
+            {{ $t('settings.notification_setting_non_followers') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+    <p>{{ $t('settings.notification_mutes') }}</p>
+    <p>{{ $t('settings.notification_blocks') }}</p>
+    <button
+      class="btn btn-default"
+      @click="updateNotificationSettings"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+</div>
+</template>
+
+<script src="./notifications.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js
new file mode 100644
index 00000000..18c44024
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.js
@@ -0,0 +1,179 @@
+import unescape from 'lodash/unescape'
+import ImageCropper from '../../image_cropper/image_cropper.vue'
+import ScopeSelector from '../../scope_selector/scope_selector.vue'
+import fileSizeFormatService from '../../../services/file_size_format/file_size_format.js'
+import ProgressButton from '../../progress_button/progress_button.vue'
+import EmojiInput from '../../emoji_input/emoji_input.vue'
+import suggestor from '../../emoji_input/suggestor.js'
+import Autosuggest from '../../autosuggest/autosuggest.vue'
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const ProfileTab = {
+  data () {
+    return {
+      newName: this.$store.state.users.currentUser.name,
+      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,
+      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,
+      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
+      pickAvatarBtnVisible: true,
+      bannerUploading: false,
+      backgroundUploading: false,
+      banner: null,
+      bannerPreview: null,
+      background: null,
+      backgroundPreview: null,
+      bannerUploadError: null,
+      backgroundUploadError: null,
+    }
+  },
+  components: {
+    ScopeSelector,
+    ImageCropper,
+    EmojiInput,
+    Autosuggest,
+    ProgressButton,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    emojiUserSuggestor () {
+      return suggestor({
+        emoji: [
+          ...this.$store.state.instance.emoji,
+          ...this.$store.state.instance.customEmoji
+        ],
+        users: this.$store.state.users.users,
+        updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
+      })
+    },
+    emojiSuggestor () {
+      return suggestor({ emoji: [
+        ...this.$store.state.instance.emoji,
+        ...this.$store.state.instance.customEmoji
+      ] })
+    }
+  },
+  methods: {
+    updateProfile () {
+      this.$store.state.api.backendInteractor
+        .updateProfile({
+          params: {
+            note: this.newBio,
+            locked: this.newLocked,
+            // Backend notation.
+            /* eslint-disable camelcase */
+            display_name: this.newName,
+            default_scope: this.newDefaultScope,
+            no_rich_text: this.newNoRichText,
+            hide_follows: this.hideFollows,
+            hide_followers: this.hideFollowers,
+            discoverable: this.discoverable,
+            allow_following_move: this.allowFollowingMove,
+            hide_follows_count: this.hideFollowsCount,
+            hide_followers_count: this.hideFollowersCount,
+            show_role: this.showRole
+            /* eslint-enable camelcase */
+          } }).then((user) => {
+          this.$store.commit('addNewUsers', [user])
+          this.$store.commit('setCurrentUser', user)
+        })
+    },
+    changeVis (visibility) {
+      this.newDefaultScope = visibility
+    },
+    uploadFile (slot, e) {
+      const file = e.target.files[0]
+      if (!file) { return }
+      if (file.size > this.$store.state.instance[slot + 'limit']) {
+        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
+        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
+        this[slot + 'UploadError'] = [
+          this.$t('upload.error.base'),
+          this.$t(
+            'upload.error.file_too_big',
+            {
+              filesize: filesize.num,
+              filesizeunit: filesize.unit,
+              allowedsize: allowedsize.num,
+              allowedsizeunit: allowedsize.unit
+            }
+          )
+        ].join(' ')
+        return
+      }
+      // eslint-disable-next-line no-undef
+      const reader = new FileReader()
+      reader.onload = ({ target }) => {
+        const img = target.result
+        this[slot + 'Preview'] = img
+        this[slot] = file
+      }
+      reader.readAsDataURL(file)
+    },
+    submitAvatar (cropper, file) {
+      const that = this
+      return new Promise((resolve, reject) => {
+        function updateAvatar (avatar) {
+          that.$store.state.api.backendInteractor.updateAvatar({ avatar })
+            .then((user) => {
+              that.$store.commit('addNewUsers', [user])
+              that.$store.commit('setCurrentUser', user)
+              resolve()
+            })
+            .catch((err) => {
+              reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
+            })
+        }
+
+        if (cropper) {
+          cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
+        } else {
+          updateAvatar(file)
+        }
+      })
+    },
+    submitBanner () {
+      if (!this.bannerPreview) { return }
+
+      this.bannerUploading = true
+      this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
+        .then((user) => {
+          this.$store.commit('addNewUsers', [user])
+          this.$store.commit('setCurrentUser', user)
+          this.bannerPreview = null
+        })
+        .catch((err) => {
+          this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
+        })
+        .then(() => { this.bannerUploading = false })
+    },
+    submitBg () {
+      if (!this.backgroundPreview) { return }
+      let background = this.background
+      this.backgroundUploading = true
+      this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
+        if (!data.error) {
+          this.$store.commit('addNewUsers', [data])
+          this.$store.commit('setCurrentUser', data)
+          this.backgroundPreview = null
+        } else {
+          this.backgroundUploadError = this.$t('upload.error.base') + data.error
+        }
+        this.backgroundUploading = false
+      })
+    }
+  }
+}
+
+export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile.scss b/src/components/settings_modal/tabs/profile.scss
new file mode 100644
index 00000000..4aab81eb
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.scss
@@ -0,0 +1,82 @@
+@import '../../../_variables.scss';
+.profile-tab {
+  .bio {
+    margin: 0;
+  }
+
+  .visibility-tray {
+    padding-top: 5px;
+  }
+
+  input[type=file] {
+    padding: 5px;
+    height: auto;
+  }
+
+  .banner {
+    max-width: 100%;
+  }
+
+  .uploading {
+    font-size: 1.5em;
+    margin: 0.25em;
+  }
+
+  .name-changer {
+    width: 100%;
+  }
+
+  .bg {
+    max-width: 100%;
+  }
+
+  .current-avatar {
+    display: block;
+    width: 150px;
+    height: 150px;
+    border-radius: $fallback--avatarRadius;
+    border-radius: var(--avatarRadius, $fallback--avatarRadius);
+  }
+
+  .oauth-tokens {
+    width: 100%;
+
+    th {
+      text-align: left;
+    }
+
+    .actions {
+      text-align: right;
+    }
+  }
+
+  &-usersearch-wrapper {
+    padding: 1em;
+  }
+
+  &-bulk-actions {
+    text-align: right;
+    padding: 0 1em;
+    min-height: 28px;
+
+    button {
+      width: 10em;
+    }
+  }
+
+  &-domain-mute-form {
+    padding: 1em;
+    display: flex;
+    flex-direction: column;
+
+    button {
+      align-self: flex-end;
+      margin-top: 1em;
+      width: 10em;
+    }
+  }
+
+  .setting-subitem {
+    margin-left: 1.75em;
+  }
+}
diff --git a/src/components/settings_modal/tabs/profile.vue b/src/components/settings_modal/tabs/profile.vue
new file mode 100644
index 00000000..335fc12e
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.vue
@@ -0,0 +1,213 @@
+<template>
+<div class="profile-tab">
+  <div class="setting-item">
+    <h2>{{ $t('settings.name_bio') }}</h2>
+    <p>{{ $t('settings.name') }}</p>
+    <EmojiInput
+      v-model="newName"
+      enable-emoji-picker
+      :suggest="emojiSuggestor"
+      >
+      <input
+        id="username"
+        v-model="newName"
+        classname="name-changer"
+        >
+    </EmojiInput>
+    <p>{{ $t('settings.bio') }}</p>
+    <EmojiInput
+      v-model="newBio"
+      enable-emoji-picker
+      :suggest="emojiUserSuggestor"
+      >
+      <textarea
+        v-model="newBio"
+        classname="bio"
+        />
+    </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'">
+          {{ $t('settings.show_admin_badge') }}
+        </template>
+        <template v-if="role === 'moderator'">
+          {{ $t('settings.show_moderator_badge') }}
+        </template>
+      </Checkbox>
+    </p>
+    <p>
+      <Checkbox v-model="discoverable">
+        {{ $t('settings.discoverable') }}
+      </Checkbox>
+    </p>
+    <button
+      :disabled="newName && newName.length === 0"
+      class="btn btn-default"
+      @click="updateProfile"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.avatar') }}</h2>
+    <p class="visibility-notice">
+      {{ $t('settings.avatar_size_instruction') }}
+    </p>
+    <p>{{ $t('settings.current_avatar') }}</p>
+    <img
+      :src="user.profile_image_url_original"
+      class="current-avatar"
+      >
+    <p>{{ $t('settings.set_new_avatar') }}</p>
+    <button
+      v-show="pickAvatarBtnVisible"
+      id="pick-avatar"
+      class="btn"
+      type="button"
+      >
+      {{ $t('settings.upload_a_photo') }}
+    </button>
+    <image-cropper
+      trigger="#pick-avatar"
+      :submit-handler="submitAvatar"
+      @open="pickAvatarBtnVisible=false"
+      @close="pickAvatarBtnVisible=true"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.profile_banner') }}</h2>
+    <p>{{ $t('settings.current_profile_banner') }}</p>
+    <img
+      :src="user.cover_photo"
+      class="banner"
+      >
+    <p>{{ $t('settings.set_new_profile_banner') }}</p>
+    <img
+      v-if="bannerPreview"
+      class="banner"
+      :src="bannerPreview"
+      >
+    <div>
+      <input
+        type="file"
+        @change="uploadFile('banner', $event)"
+        >
+    </div>
+    <i
+      v-if="bannerUploading"
+      class=" icon-spin4 animate-spin uploading"
+      />
+    <button
+      v-else-if="bannerPreview"
+      class="btn btn-default"
+      @click="submitBanner"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <div
+      v-if="bannerUploadError"
+      class="alert error"
+      >
+      Error: {{ bannerUploadError }}
+      <i
+        class="button-icon icon-cancel"
+        @click="clearUploadError('banner')"
+        />
+    </div>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.profile_background') }}</h2>
+    <p>{{ $t('settings.set_new_profile_background') }}</p>
+    <img
+      v-if="backgroundPreview"
+      class="bg"
+      :src="backgroundPreview"
+      >
+    <div>
+      <input
+        type="file"
+        @change="uploadFile('background', $event)"
+        >
+    </div>
+    <i
+      v-if="backgroundUploading"
+      class=" icon-spin4 animate-spin uploading"
+      />
+    <button
+      v-else-if="backgroundPreview"
+      class="btn btn-default"
+      @click="submitBg"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <div
+      v-if="backgroundUploadError"
+      class="alert error"
+      >
+      Error: {{ backgroundUploadError }}
+      <i
+        class="button-icon icon-cancel"
+        @click="clearUploadError('background')"
+        />
+    </div>
+  </div>
+</div>
+</template>
+
+<script src="./profile.js"></script>
+<style lang="scss" src="./profile.scss"></style>
diff --git a/src/components/settings_modal/tabs/security.js b/src/components/settings_modal/tabs/security.js
new file mode 100644
index 00000000..cc791b7a
--- /dev/null
+++ b/src/components/settings_modal/tabs/security.js
@@ -0,0 +1,106 @@
+import ProgressButton from '../../progress_button/progress_button.vue'
+import Checkbox from '../../checkbox/checkbox.vue'
+import Mfa from '../../user_settings/mfa.vue'
+
+const Security = {
+  data () {
+    return {
+      newEmail: '',
+      changeEmailError: false,
+      changeEmailPassword: '',
+      changedEmail: false,
+      deletingAccount: false,
+      deleteAccountConfirmPasswordInput: '',
+      deleteAccountError: false,
+      changePasswordInputs: [ '', '', '' ],
+      changedPassword: false,
+      changePasswordError: false
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    ProgressButton,
+    Mfa,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    pleromaBackend () {
+      return this.$store.state.instance.pleromaBackend
+    },
+    oauthTokens () {
+      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
+        return {
+          id: oauthToken.id,
+          appName: oauthToken.app_name,
+          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+        }
+      })
+    }
+  },
+  methods: {
+    confirmDelete () {
+      this.deletingAccount = true
+    },
+    deleteAccount () {
+      this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
+        .then((res) => {
+          if (res.status === 'success') {
+            this.$store.dispatch('logout')
+            this.$router.push({ name: 'root' })
+          } else {
+            this.deleteAccountError = res.error
+          }
+        })
+    },
+    changePassword () {
+      const params = {
+        password: this.changePasswordInputs[0],
+        newPassword: this.changePasswordInputs[1],
+        newPasswordConfirmation: this.changePasswordInputs[2]
+      }
+      this.$store.state.api.backendInteractor.changePassword(params)
+        .then((res) => {
+          if (res.status === 'success') {
+            this.changedPassword = true
+            this.changePasswordError = false
+            this.logout()
+          } else {
+            this.changedPassword = false
+            this.changePasswordError = res.error
+          }
+        })
+    },
+    changeEmail () {
+      const params = {
+        email: this.newEmail,
+        password: this.changeEmailPassword
+      }
+      this.$store.state.api.backendInteractor.changeEmail(params)
+        .then((res) => {
+          if (res.status === 'success') {
+            this.changedEmail = true
+            this.changeEmailError = false
+          } else {
+            this.changedEmail = false
+            this.changeEmailError = res.error
+          }
+        })
+    },
+    logout () {
+      this.$store.dispatch('logout')
+      this.$router.replace('/')
+    },
+    revokeToken (id) {
+      if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
+        this.$store.dispatch('revokeToken', id)
+      }
+    }
+  }
+}
+
+export default Security
diff --git a/src/components/settings_modal/tabs/security.vue b/src/components/settings_modal/tabs/security.vue
new file mode 100644
index 00000000..603c9a04
--- /dev/null
+++ b/src/components/settings_modal/tabs/security.vue
@@ -0,0 +1,143 @@
+<template>
+<div :label="$t('settings.security_tab')">
+  <div class="setting-item">
+    <h2>{{ $t('settings.change_email') }}</h2>
+    <div>
+      <p>{{ $t('settings.new_email') }}</p>
+      <input
+        v-model="newEmail"
+        type="email"
+        autocomplete="email"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.current_password') }}</p>
+      <input
+        v-model="changeEmailPassword"
+        type="password"
+        autocomplete="current-password"
+        >
+    </div>
+    <button
+      class="btn btn-default"
+      @click="changeEmail"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <p v-if="changedEmail">
+      {{ $t('settings.changed_email') }}
+    </p>
+    <template v-if="changeEmailError !== false">
+      <p>{{ $t('settings.change_email_error') }}</p>
+      <p>{{ changeEmailError }}</p>
+    </template>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.change_password') }}</h2>
+    <div>
+      <p>{{ $t('settings.current_password') }}</p>
+      <input
+        v-model="changePasswordInputs[0]"
+        type="password"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.new_password') }}</p>
+      <input
+        v-model="changePasswordInputs[1]"
+        type="password"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.confirm_new_password') }}</p>
+      <input
+        v-model="changePasswordInputs[2]"
+        type="password"
+        >
+    </div>
+    <button
+      class="btn btn-default"
+      @click="changePassword"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <p v-if="changedPassword">
+      {{ $t('settings.changed_password') }}
+    </p>
+    <p v-else-if="changePasswordError !== false">
+      {{ $t('settings.change_password_error') }}
+    </p>
+    <p v-if="changePasswordError">
+      {{ changePasswordError }}
+    </p>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.oauth_tokens') }}</h2>
+    <table class="oauth-tokens">
+      <thead>
+        <tr>
+          <th>{{ $t('settings.app_name') }}</th>
+          <th>{{ $t('settings.valid_until') }}</th>
+          <th />
+        </tr>
+      </thead>
+      <tbody>
+        <tr
+          v-for="oauthToken in oauthTokens"
+          :key="oauthToken.id"
+          >
+          <td>{{ oauthToken.appName }}</td>
+          <td>{{ oauthToken.validUntil }}</td>
+          <td class="actions">
+            <button
+              class="btn btn-default"
+              @click="revokeToken(oauthToken.id)"
+              >
+              {{ $t('settings.revoke_token') }}
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <mfa />
+  <div class="setting-item">
+    <h2>{{ $t('settings.delete_account') }}</h2>
+    <p v-if="!deletingAccount">
+      {{ $t('settings.delete_account_description') }}
+    </p>
+    <div v-if="deletingAccount">
+      <p>{{ $t('settings.delete_account_instructions') }}</p>
+      <p>{{ $t('login.password') }}</p>
+      <input
+        v-model="deleteAccountConfirmPasswordInput"
+        type="password"
+        >
+      <button
+        class="btn btn-default"
+        @click="deleteAccount"
+        >
+        {{ $t('settings.delete_account') }}
+      </button>
+    </div>
+    <p v-if="deleteAccountError !== false">
+      {{ $t('settings.delete_account_error') }}
+    </p>
+    <p v-if="deleteAccountError">
+      {{ deleteAccountError }}
+    </p>
+    <button
+      v-if="!deletingAccount"
+      class="btn btn-default"
+      @click="confirmDelete"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+</div>
+</template>
+
+<script src="./security.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 008e1e95..97791de3 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
       required: false,
       type: Boolean,
       default: false
+    },
+    sideTabBar: {
+      required: false,
+      type: Boolean,
+      default: false
     }
   },
   data () {
@@ -105,7 +110,7 @@ export default Vue.component('tab-switcher', {
     })
 
     return (
-      <div class="tab-switcher">
+      <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
         <div class="tabs">
           {tabs}
         </div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index df585faa..a443531e 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -2,7 +2,116 @@
 
 .tab-switcher {
   display: flex;
-  flex-direction: column;
+
+  &.top-tabs {
+    flex-direction: column;
+    > .tabs {
+      width: 100%;
+      overflow-y: hidden;
+      overflow-x: auto;
+      padding-top: 5px;
+      flex-direction: row;
+      &::after, &::before {
+        content: '';
+        flex: 1 1 auto;
+        border-bottom: 1px solid;
+        border-bottom-color: $fallback--border;
+        border-bottom-color: var(--border, $fallback--border);
+      }
+      .tab-wrapper {
+        height: 28px;
+
+        &:not(.active)::after {
+          left: 0;
+          right: 0;
+          bottom: 0;
+          border-bottom: 1px solid;
+          border-bottom-color: $fallback--border;
+          border-bottom-color: var(--border, $fallback--border);
+        }
+      }
+      .tab {
+        width: 100%;
+        min-width: 1px;
+        border-bottom-left-radius: 0;
+        border-bottom-right-radius: 0;
+        padding-bottom: 99px;
+        margin-bottom: 6px - 99px;
+      }
+    }
+    .contents.scrollable-tabs {
+      flex-basis: 0;
+    }
+  }
+
+  &.side-tabs {
+    flex-direction: row;
+    > .contents {
+      flex: 0 1 80%;
+    }
+    > .tabs {
+      flex: 1 0 auto;
+      overflow-y: auto;
+      overflow-x: hidden;
+      padding-top: 5px;
+      flex-direction: column;
+      &::after {
+        content: '';
+        flex: 1 1 auto;
+        border-right: 1px solid;
+        border-right-color: $fallback--border;
+        border-right-color: var(--border, $fallback--border);
+      }
+      .tab-wrapper {
+        min-width: 10em;
+        &:not(.active)::after {
+          top: 0;
+          right: 0;
+          bottom: 0;
+          border-right: 1px solid;
+          border-right-color: $fallback--border;
+          border-right-color: var(--border, $fallback--border);
+        }
+      }
+      .tab {
+        box-sizing: content-box;
+        width: 100%;
+        margin-bottom: 5px;
+        min-width: 10em;
+        min-width: 1px;
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;
+        // padding-right: 200px;
+        // margin-right: 6px - 200px;
+        margin-left: 6px;
+      }
+
+      .tab-wrapper {
+        min-width: 10em;
+        &:not(.active)::after {
+          top: 0;
+          right: 0;
+          bottom: 0;
+          border-right: 1px solid;
+          border-right-color: $fallback--border;
+          border-right-color: var(--border, $fallback--border);
+        }
+      }
+      .tab {
+        box-sizing: content-box;
+        width: 100%;
+        margin-bottom: 5px;
+        min-width: 10em;
+        min-width: 1px;
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;
+        // padding-right: 200px;
+        // margin-right: 6px - 200px;
+        margin-left: 6px;
+      }
+    }
+  }
+
 
   .contents {
     flex: 1 0 auto;
@@ -13,86 +122,65 @@
     }
 
     &.scrollable-tabs {
-      flex-basis: 0;
       overflow-y: auto;
     }
   }
+
+  .tab {
+    position: relative;
+    white-space: nowrap;
+
+    padding: 6px 1em;
+    color: $fallback--text;
+    color: var(--tabText, $fallback--text);
+    background-color: $fallback--fg;
+    background-color: var(--tab, $fallback--fg);
+
+    &:not(.active) {
+      z-index: 4;
+
+      &:hover {
+        z-index: 6;
+      }
+    }
+
+    &.active {
+      background: transparent;
+      z-index: 5;
+      color: $fallback--text;
+      color: var(--tabActiveText, $fallback--text);
+    }
+
+    img {
+      max-height: 26px;
+      vertical-align: top;
+      margin-top: -5px;
+    }
+  }
+
+
   .tabs {
     display: flex;
     position: relative;
-    width: 100%;
-    overflow-y: hidden;
-    overflow-x: auto;
-    padding-top: 5px;
     box-sizing: border-box;
 
     &::after, &::before {
       display: block;
-      content: '';
       flex: 1 1 auto;
-      border-bottom: 1px solid;
-      border-bottom-color: $fallback--border;
-      border-bottom-color: var(--border, $fallback--border);
     }
+  }
 
-    .tab-wrapper {
-      height: 28px;
-      position: relative;
-      display: flex;
-      flex: 0 0 auto;
-
-      .tab {
-        width: 100%;
-        min-width: 1px;
-        position: relative;
-        border-bottom-left-radius: 0;
-        border-bottom-right-radius: 0;
-        padding: 6px 1em;
-        padding-bottom: 99px;
-        margin-bottom: 6px - 99px;
-        white-space: nowrap;
-
-        color: $fallback--text;
-        color: var(--tabText, $fallback--text);
-        background-color: $fallback--fg;
-        background-color: var(--tab, $fallback--fg);
-
-        &:not(.active) {
-          z-index: 4;
-
-          &:hover {
-            z-index: 6;
-          }
-        }
-
-        &.active {
-          background: transparent;
-          z-index: 5;
-          color: $fallback--text;
-          color: var(--tabActiveText, $fallback--text);
-        }
-
-        img {
-          max-height: 26px;
-          vertical-align: top;
-          margin-top: -5px;
-        }
-      }
+  .tab-wrapper {
+    position: relative;
+    display: flex;
+    flex: 0 0 auto;
 
-      &:not(.active) {
-        &::after {
-          content: '';
-          position: absolute;
-          left: 0;
-          right: 0;
-          bottom: 0;
-          z-index: 7;
-          border-bottom: 1px solid;
-          border-bottom-color: $fallback--border;
-          border-bottom-color: var(--border, $fallback--border);
-        }
+    &:not(.active) {
+      &::after {
+        content: '';
+        position: absolute;
+        z-index: 7;
       }
     }
-
   }
 }
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index eca6f9b1..e07d4e56 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,25 +1,17 @@
-import unescape from 'lodash/unescape'
 import get from 'lodash/get'
 import map from 'lodash/map'
 import reject from 'lodash/reject'
+import Autosuggest from '../autosuggest/autosuggest.vue'
 import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import ImageCropper from '../image_cropper/image_cropper.vue'
-import StyleSwitcher from '../style_switcher/style_switcher.vue'
-import ScopeSelector from '../scope_selector/scope_selector.vue'
-import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
 import BlockCard from '../block_card/block_card.vue'
 import MuteCard from '../mute_card/mute_card.vue'
 import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
 import SelectableList from '../selectable_list/selectable_list.vue'
 import ProgressButton from '../progress_button/progress_button.vue'
-import EmojiInput from '../emoji_input/emoji_input.vue'
-import suggestor from '../emoji_input/suggestor.js'
-import Autosuggest from '../autosuggest/autosuggest.vue'
 import Importer from '../importer/importer.vue'
 import Exporter from '../exporter/exporter.vue'
 import withSubscription from '../../hocs/with_subscription/with_subscription'
 import Checkbox from '../checkbox/checkbox.vue'
-import Mfa from './mfa.vue'
 
 const BlockList = withSubscription({
   fetch: (props, $store) => $store.dispatch('fetchBlocks'),
@@ -42,40 +34,7 @@ const DomainMuteList = withSubscription({
 const UserSettings = {
   data () {
     return {
-      newEmail: '',
-      newName: this.$store.state.users.currentUser.name,
-      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,
-      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,
-      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
-      pickAvatarBtnVisible: true,
-      bannerUploading: false,
-      backgroundUploading: false,
-      banner: null,
-      bannerPreview: null,
-      background: null,
-      backgroundPreview: null,
-      bannerUploadError: null,
-      backgroundUploadError: null,
-      changeEmailError: false,
-      changeEmailPassword: '',
-      changedEmail: false,
-      deletingAccount: false,
-      deleteAccountConfirmPasswordInput: '',
-      deleteAccountError: false,
-      changePasswordInputs: [ '', '', '' ],
-      changedPassword: false,
-      changePasswordError: false,
       activeTab: 'profile',
-      notificationSettings: this.$store.state.users.currentUser.notification_settings,
       newDomainToMute: ''
     }
   },
@@ -83,176 +42,29 @@ const UserSettings = {
     this.$store.dispatch('fetchTokens')
   },
   components: {
-    StyleSwitcher,
-    ScopeSelector,
     TabSwitcher,
-    ImageCropper,
     BlockList,
     MuteList,
     DomainMuteList,
-    EmojiInput,
-    Autosuggest,
     BlockCard,
     MuteCard,
     DomainMuteCard,
     ProgressButton,
-    Importer,
-    Exporter,
-    Mfa,
+    Autosuggest,
     Checkbox
   },
   computed: {
     user () {
       return this.$store.state.users.currentUser
     },
-    emojiUserSuggestor () {
-      return suggestor({
-        emoji: [
-          ...this.$store.state.instance.emoji,
-          ...this.$store.state.instance.customEmoji
-        ],
-        users: this.$store.state.users.users,
-        updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
-      })
-    },
-    emojiSuggestor () {
-      return suggestor({ emoji: [
-        ...this.$store.state.instance.emoji,
-        ...this.$store.state.instance.customEmoji
-      ] })
-    },
     pleromaBackend () {
       return this.$store.state.instance.pleromaBackend
     },
-    minimalScopesMode () {
-      return this.$store.state.instance.minimalScopesMode
-    },
-    vis () {
-      return {
-        public: { selected: this.newDefaultScope === 'public' },
-        unlisted: { selected: this.newDefaultScope === 'unlisted' },
-        private: { selected: this.newDefaultScope === 'private' },
-        direct: { selected: this.newDefaultScope === 'direct' }
-      }
-    },
     currentSaveStateNotice () {
       return this.$store.state.interface.settings.currentSaveStateNotice
-    },
-    oauthTokens () {
-      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
-        return {
-          id: oauthToken.id,
-          appName: oauthToken.app_name,
-          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
-        }
-      })
     }
   },
   methods: {
-    updateProfile () {
-      this.$store.state.api.backendInteractor
-        .updateProfile({
-          params: {
-            note: this.newBio,
-            locked: this.newLocked,
-            // Backend notation.
-            /* eslint-disable camelcase */
-            display_name: this.newName,
-            default_scope: this.newDefaultScope,
-            no_rich_text: this.newNoRichText,
-            hide_follows: this.hideFollows,
-            hide_followers: this.hideFollowers,
-            discoverable: this.discoverable,
-            allow_following_move: this.allowFollowingMove,
-            hide_follows_count: this.hideFollowsCount,
-            hide_followers_count: this.hideFollowersCount,
-            show_role: this.showRole
-            /* eslint-enable camelcase */
-          } }).then((user) => {
-          this.$store.commit('addNewUsers', [user])
-          this.$store.commit('setCurrentUser', user)
-        })
-    },
-    updateNotificationSettings () {
-      this.$store.state.api.backendInteractor
-        .updateNotificationSettings({ settings: this.notificationSettings })
-    },
-    changeVis (visibility) {
-      this.newDefaultScope = visibility
-    },
-    uploadFile (slot, e) {
-      const file = e.target.files[0]
-      if (!file) { return }
-      if (file.size > this.$store.state.instance[slot + 'limit']) {
-        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
-        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
-        this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
-        return
-      }
-      // eslint-disable-next-line no-undef
-      const reader = new FileReader()
-      reader.onload = ({ target }) => {
-        const img = target.result
-        this[slot + 'Preview'] = img
-        this[slot] = file
-      }
-      reader.readAsDataURL(file)
-    },
-    submitAvatar (cropper, file) {
-      const that = this
-      return new Promise((resolve, reject) => {
-        function updateAvatar (avatar) {
-          that.$store.state.api.backendInteractor.updateAvatar({ avatar })
-            .then((user) => {
-              that.$store.commit('addNewUsers', [user])
-              that.$store.commit('setCurrentUser', user)
-              resolve()
-            })
-            .catch((err) => {
-              reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
-            })
-        }
-
-        if (cropper) {
-          cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
-        } else {
-          updateAvatar(file)
-        }
-      })
-    },
-    clearUploadError (slot) {
-      this[slot + 'UploadError'] = null
-    },
-    submitBanner () {
-      if (!this.bannerPreview) { return }
-
-      this.bannerUploading = true
-      this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
-        .then((user) => {
-          this.$store.commit('addNewUsers', [user])
-          this.$store.commit('setCurrentUser', user)
-          this.bannerPreview = null
-        })
-        .catch((err) => {
-          this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
-        })
-        .then(() => { this.bannerUploading = false })
-    },
-    submitBg () {
-      if (!this.backgroundPreview) { return }
-      let background = this.background
-      this.backgroundUploading = true
-      this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
-        if (!data.error) {
-          this.$store.commit('addNewUsers', [data])
-          this.$store.commit('setCurrentUser', data)
-          this.backgroundPreview = null
-        } else {
-          this.backgroundUploadError = this.$t('upload.error.base') + data.error
-        }
-        this.backgroundUploading = false
-      })
-    },
     importFollows (file) {
       return this.$store.state.api.backendInteractor.importFollows({ file })
         .then((status) => {
@@ -281,74 +93,9 @@ const UserSettings = {
         return user.screen_name
       }).join('\n')
     },
-    getFollowsContent () {
-      return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
-        .then(this.generateExportableUsersContent)
-    },
-    getBlocksContent () {
-      return this.$store.state.api.backendInteractor.fetchBlocks()
-        .then(this.generateExportableUsersContent)
-    },
-    confirmDelete () {
-      this.deletingAccount = true
-    },
-    deleteAccount () {
-      this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
-        .then((res) => {
-          if (res.status === 'success') {
-            this.$store.dispatch('logout')
-            this.$router.push({ name: 'root' })
-          } else {
-            this.deleteAccountError = res.error
-          }
-        })
-    },
-    changePassword () {
-      const params = {
-        password: this.changePasswordInputs[0],
-        newPassword: this.changePasswordInputs[1],
-        newPasswordConfirmation: this.changePasswordInputs[2]
-      }
-      this.$store.state.api.backendInteractor.changePassword(params)
-        .then((res) => {
-          if (res.status === 'success') {
-            this.changedPassword = true
-            this.changePasswordError = false
-            this.logout()
-          } else {
-            this.changedPassword = false
-            this.changePasswordError = res.error
-          }
-        })
-    },
-    changeEmail () {
-      const params = {
-        email: this.newEmail,
-        password: this.changeEmailPassword
-      }
-      this.$store.state.api.backendInteractor.changeEmail(params)
-        .then((res) => {
-          if (res.status === 'success') {
-            this.changedEmail = true
-            this.changeEmailError = false
-          } else {
-            this.changedEmail = false
-            this.changeEmailError = res.error
-          }
-        })
-    },
     activateTab (tabName) {
       this.activeTab = tabName
     },
-    logout () {
-      this.$store.dispatch('logout')
-      this.$router.replace('/')
-    },
-    revokeToken (id) {
-      if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
-        this.$store.dispatch('revokeToken', id)
-      }
-    },
     filterUnblockedUsers (userIds) {
       return reject(userIds, (userId) => {
         const user = this.$store.getters.findUser(userId)
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8b2336b4..2a88714f 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -25,603 +25,6 @@
       </transition>
     </div>
     <div class="panel-body profile-edit">
-      <tab-switcher>
-        <div :label="$t('settings.profile_tab')">
-          <div class="setting-item">
-            <h2>{{ $t('settings.name_bio') }}</h2>
-            <p>{{ $t('settings.name') }}</p>
-            <EmojiInput
-              v-model="newName"
-              enable-emoji-picker
-              :suggest="emojiSuggestor"
-            >
-              <input
-                id="username"
-                v-model="newName"
-                classname="name-changer"
-              >
-            </EmojiInput>
-            <p>{{ $t('settings.bio') }}</p>
-            <EmojiInput
-              v-model="newBio"
-              enable-emoji-picker
-              :suggest="emojiUserSuggestor"
-            >
-              <textarea
-                v-model="newBio"
-                classname="bio"
-              />
-            </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'">
-                  {{ $t('settings.show_admin_badge') }}
-                </template>
-                <template v-if="role === 'moderator'">
-                  {{ $t('settings.show_moderator_badge') }}
-                </template>
-              </Checkbox>
-            </p>
-            <p>
-              <Checkbox v-model="discoverable">
-                {{ $t('settings.discoverable') }}
-              </Checkbox>
-            </p>
-            <button
-              :disabled="newName && newName.length === 0"
-              class="btn btn-default"
-              @click="updateProfile"
-            >
-              {{ $t('general.submit') }}
-            </button>
-          </div>
-          <div class="setting-item">
-            <h2>{{ $t('settings.avatar') }}</h2>
-            <p class="visibility-notice">
-              {{ $t('settings.avatar_size_instruction') }}
-            </p>
-            <p>{{ $t('settings.current_avatar') }}</p>
-            <img
-              :src="user.profile_image_url_original"
-              class="current-avatar"
-            >
-            <p>{{ $t('settings.set_new_avatar') }}</p>
-            <button
-              v-show="pickAvatarBtnVisible"
-              id="pick-avatar"
-              class="btn"
-              type="button"
-            >
-              {{ $t('settings.upload_a_photo') }}
-            </button>
-            <image-cropper
-              trigger="#pick-avatar"
-              :submit-handler="submitAvatar"
-              @open="pickAvatarBtnVisible=false"
-              @close="pickAvatarBtnVisible=true"
-            />
-          </div>
-          <div class="setting-item">
-            <h2>{{ $t('settings.profile_banner') }}</h2>
-            <p>{{ $t('settings.current_profile_banner') }}</p>
-            <img
-              :src="user.cover_photo"
-              class="banner"
-            >
-            <p>{{ $t('settings.set_new_profile_banner') }}</p>
-            <img
-              v-if="bannerPreview"
-              class="banner"
-              :src="bannerPreview"
-            >
-            <div>
-              <input
-                type="file"
-                @change="uploadFile('banner', $event)"
-              >
-            </div>
-            <i
-              v-if="bannerUploading"
-              class=" icon-spin4 animate-spin uploading"
-            />
-            <button
-              v-else-if="bannerPreview"
-              class="btn btn-default"
-              @click="submitBanner"
-            >
-              {{ $t('general.submit') }}
-            </button>
-            <div
-              v-if="bannerUploadError"
-              class="alert error"
-            >
-              Error: {{ bannerUploadError }}
-              <i
-                class="button-icon icon-cancel"
-                @click="clearUploadError('banner')"
-              />
-            </div>
-          </div>
-          <div class="setting-item">
-            <h2>{{ $t('settings.profile_background') }}</h2>
-            <p>{{ $t('settings.set_new_profile_background') }}</p>
-            <img
-              v-if="backgroundPreview"
-              class="bg"
-              :src="backgroundPreview"
-            >
-            <div>
-              <input
-                type="file"
-                @change="uploadFile('background', $event)"
-              >
-            </div>
-            <i
-              v-if="backgroundUploading"
-              class=" icon-spin4 animate-spin uploading"
-            />
-            <button
-              v-else-if="backgroundPreview"
-              class="btn btn-default"
-              @click="submitBg"
-            >
-              {{ $t('general.submit') }}
-            </button>
-            <div
-              v-if="backgroundUploadError"
-              class="alert error"
-            >
-              Error: {{ backgroundUploadError }}
-              <i
-                class="button-icon icon-cancel"
-                @click="clearUploadError('background')"
-              />
-            </div>
-          </div>
-        </div>
-
-        <div :label="$t('settings.security_tab')">
-          <div class="setting-item">
-            <h2>{{ $t('settings.change_email') }}</h2>
-            <div>
-              <p>{{ $t('settings.new_email') }}</p>
-              <input
-                v-model="newEmail"
-                type="email"
-                autocomplete="email"
-              >
-            </div>
-            <div>
-              <p>{{ $t('settings.current_password') }}</p>
-              <input
-                v-model="changeEmailPassword"
-                type="password"
-                autocomplete="current-password"
-              >
-            </div>
-            <button
-              class="btn btn-default"
-              @click="changeEmail"
-            >
-              {{ $t('general.submit') }}
-            </button>
-            <p v-if="changedEmail">
-              {{ $t('settings.changed_email') }}
-            </p>
-            <template v-if="changeEmailError !== false">
-              <p>{{ $t('settings.change_email_error') }}</p>
-              <p>{{ changeEmailError }}</p>
-            </template>
-          </div>
-
-          <div class="setting-item">
-            <h2>{{ $t('settings.change_password') }}</h2>
-            <div>
-              <p>{{ $t('settings.current_password') }}</p>
-              <input
-                v-model="changePasswordInputs[0]"
-                type="password"
-              >
-            </div>
-            <div>
-              <p>{{ $t('settings.new_password') }}</p>
-              <input
-                v-model="changePasswordInputs[1]"
-                type="password"
-              >
-            </div>
-            <div>
-              <p>{{ $t('settings.confirm_new_password') }}</p>
-              <input
-                v-model="changePasswordInputs[2]"
-                type="password"
-              >
-            </div>
-            <button
-              class="btn btn-default"
-              @click="changePassword"
-            >
-              {{ $t('general.submit') }}
-            </button>
-            <p v-if="changedPassword">
-              {{ $t('settings.changed_password') }}
-            </p>
-            <p v-else-if="changePasswordError !== false">
-              {{ $t('settings.change_password_error') }}
-            </p>
-            <p v-if="changePasswordError">
-              {{ changePasswordError }}
-            </p>
-          </div>
-
-          <div class="setting-item">
-            <h2>{{ $t('settings.oauth_tokens') }}</h2>
-            <table class="oauth-tokens">
-              <thead>
-                <tr>
-                  <th>{{ $t('settings.app_name') }}</th>
-                  <th>{{ $t('settings.valid_until') }}</th>
-                  <th />
-                </tr>
-              </thead>
-              <tbody>
-                <tr
-                  v-for="oauthToken in oauthTokens"
-                  :key="oauthToken.id"
-                >
-                  <td>{{ oauthToken.appName }}</td>
-                  <td>{{ oauthToken.validUntil }}</td>
-                  <td class="actions">
-                    <button
-                      class="btn btn-default"
-                      @click="revokeToken(oauthToken.id)"
-                    >
-                      {{ $t('settings.revoke_token') }}
-                    </button>
-                  </td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-          <mfa />
-          <div class="setting-item">
-            <h2>{{ $t('settings.delete_account') }}</h2>
-            <p v-if="!deletingAccount">
-              {{ $t('settings.delete_account_description') }}
-            </p>
-            <div v-if="deletingAccount">
-              <p>{{ $t('settings.delete_account_instructions') }}</p>
-              <p>{{ $t('login.password') }}</p>
-              <input
-                v-model="deleteAccountConfirmPasswordInput"
-                type="password"
-              >
-              <button
-                class="btn btn-default"
-                @click="deleteAccount"
-              >
-                {{ $t('settings.delete_account') }}
-              </button>
-            </div>
-            <p v-if="deleteAccountError !== false">
-              {{ $t('settings.delete_account_error') }}
-            </p>
-            <p v-if="deleteAccountError">
-              {{ deleteAccountError }}
-            </p>
-            <button
-              v-if="!deletingAccount"
-              class="btn btn-default"
-              @click="confirmDelete"
-            >
-              {{ $t('general.submit') }}
-            </button>
-          </div>
-        </div>
-
-        <div
-          v-if="pleromaBackend"
-          :label="$t('settings.notifications')"
-        >
-          <div class="setting-item">
-            <div class="select-multiple">
-              <span class="label">{{ $t('settings.notification_setting') }}</span>
-              <ul class="option-list">
-                <li>
-                  <Checkbox v-model="notificationSettings.follows">
-                    {{ $t('settings.notification_setting_follows') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="notificationSettings.followers">
-                    {{ $t('settings.notification_setting_followers') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="notificationSettings.non_follows">
-                    {{ $t('settings.notification_setting_non_follows') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="notificationSettings.non_followers">
-                    {{ $t('settings.notification_setting_non_followers') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-            <p>{{ $t('settings.notification_mutes') }}</p>
-            <p>{{ $t('settings.notification_blocks') }}</p>
-            <button
-              class="btn btn-default"
-              @click="updateNotificationSettings"
-            >
-              {{ $t('general.submit') }}
-            </button>
-          </div>
-        </div>
-
-        <div
-          v-if="pleromaBackend"
-          :label="$t('settings.data_import_export_tab')"
-        >
-          <div class="setting-item">
-            <h2>{{ $t('settings.follow_import') }}</h2>
-            <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
-            <Importer
-              :submit-handler="importFollows"
-              :success-message="$t('settings.follows_imported')"
-              :error-message="$t('settings.follow_import_error')"
-            />
-          </div>
-          <div class="setting-item">
-            <h2>{{ $t('settings.follow_export') }}</h2>
-            <Exporter
-              :get-content="getFollowsContent"
-              filename="friends.csv"
-              :export-button-label="$t('settings.follow_export_button')"
-            />
-          </div>
-          <div class="setting-item">
-            <h2>{{ $t('settings.block_import') }}</h2>
-            <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
-            <Importer
-              :submit-handler="importBlocks"
-              :success-message="$t('settings.blocks_imported')"
-              :error-message="$t('settings.block_import_error')"
-            />
-          </div>
-          <div class="setting-item">
-            <h2>{{ $t('settings.block_export') }}</h2>
-            <Exporter
-              :get-content="getBlocksContent"
-              filename="blocks.csv"
-              :export-button-label="$t('settings.block_export_button')"
-            />
-          </div>
-        </div>
-
-        <div :label="$t('settings.blocks_tab')">
-          <div class="profile-edit-usersearch-wrapper">
-            <Autosuggest
-              :filter="filterUnblockedUsers"
-              :query="queryUserIds"
-              :placeholder="$t('settings.search_user_to_block')"
-            >
-              <BlockCard
-                slot-scope="row"
-                :user-id="row.item"
-              />
-            </Autosuggest>
-          </div>
-          <BlockList
-            :refresh="true"
-            :get-key="identity"
-          >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-            >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => blockUsers(selected)"
-                >
-                  {{ $t('user_card.block') }}
-                  <template slot="progress">
-                    {{ $t('user_card.block_progress') }}
-                  </template>
-                </ProgressButton>
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => unblockUsers(selected)"
-                >
-                  {{ $t('user_card.unblock') }}
-                  <template slot="progress">
-                    {{ $t('user_card.unblock_progress') }}
-                  </template>
-                </ProgressButton>
-              </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-            >
-              <BlockCard :user-id="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_blocks') }}
-            </template>
-          </BlockList>
-        </div>
-
-        <div :label="$t('settings.mutes_tab')">
-          <tab-switcher>
-            <div label="Users">
-              <div class="profile-edit-usersearch-wrapper">
-                <Autosuggest
-                  :filter="filterUnMutedUsers"
-                  :query="queryUserIds"
-                  :placeholder="$t('settings.search_user_to_mute')"
-                >
-                  <MuteCard
-                    slot-scope="row"
-                    :user-id="row.item"
-                  />
-                </Autosuggest>
-              </div>
-              <MuteList
-                :refresh="true"
-                :get-key="identity"
-              >
-                <template
-                  slot="header"
-                  slot-scope="{selected}"
-                >
-                  <div class="profile-edit-bulk-actions">
-                    <ProgressButton
-                      v-if="selected.length > 0"
-                      class="btn btn-default"
-                      :click="() => muteUsers(selected)"
-                    >
-                      {{ $t('user_card.mute') }}
-                      <template slot="progress">
-                        {{ $t('user_card.mute_progress') }}
-                      </template>
-                    </ProgressButton>
-                    <ProgressButton
-                      v-if="selected.length > 0"
-                      class="btn btn-default"
-                      :click="() => unmuteUsers(selected)"
-                    >
-                      {{ $t('user_card.unmute') }}
-                      <template slot="progress">
-                        {{ $t('user_card.unmute_progress') }}
-                      </template>
-                    </ProgressButton>
-                  </div>
-                </template>
-                <template
-                  slot="item"
-                  slot-scope="{item}"
-                >
-                  <MuteCard :user-id="item" />
-                </template>
-                <template slot="empty">
-                  {{ $t('settings.no_mutes') }}
-                </template>
-              </MuteList>
-            </div>
-
-            <div :label="$t('settings.domain_mutes')">
-              <div class="profile-edit-domain-mute-form">
-                <input
-                  v-model="newDomainToMute"
-                  :placeholder="$t('settings.type_domains_to_mute')"
-                  type="text"
-                  @keyup.enter="muteDomain"
-                >
-                <ProgressButton
-                  class="btn btn-default"
-                  :click="muteDomain"
-                >
-                  {{ $t('domain_mute_card.mute') }}
-                  <template slot="progress">
-                    {{ $t('domain_mute_card.mute_progress') }}
-                  </template>
-                </ProgressButton>
-              </div>
-              <DomainMuteList
-                :refresh="true"
-                :get-key="identity"
-              >
-                <template
-                  slot="header"
-                  slot-scope="{selected}"
-                >
-                  <div class="profile-edit-bulk-actions">
-                    <ProgressButton
-                      v-if="selected.length > 0"
-                      class="btn btn-default"
-                      :click="() => unmuteDomains(selected)"
-                    >
-                      {{ $t('domain_mute_card.unmute') }}
-                      <template slot="progress">
-                        {{ $t('domain_mute_card.unmute_progress') }}
-                      </template>
-                    </ProgressButton>
-                  </div>
-                </template>
-                <template
-                  slot="item"
-                  slot-scope="{item}"
-                >
-                  <DomainMuteCard :domain="item" />
-                </template>
-                <template slot="empty">
-                  {{ $t('settings.no_mutes') }}
-                </template>
-              </DomainMuteList>
-            </div>
-          </tab-switcher>
-        </div>
-      </tab-switcher>
     </div>
   </div>
 </template>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 37d9591c..312f7283 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -278,6 +278,7 @@
     "current_avatar": "Your current avatar",
     "current_password": "Current password",
     "current_profile_banner": "Your current profile banner",
+    "mutes_and_blocks": "Mutes and Blocks",
     "data_import_export_tab": "Data Import / Export",
     "default_vis": "Default visibility scope",
     "delete_account": "Delete Account",
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 5b2762e5..e55b7290 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,6 +1,7 @@
 import { set, delete as del } from 'vue'
 
 const defaultState = {
+  settingsModalState: 'hidden',
   settings: {
     currentSaveStateNotice: null,
     noticeClearTimeout: null,
@@ -35,6 +36,24 @@ const interfaceMod = {
     },
     setMobileLayout (state, value) {
       state.mobileLayout = value
+    },
+    closeSettingsModal (state) {
+      state.settingsModalState = 'hidden'
+    },
+    togglePeekSettingsModal (state) {
+      switch (state.settingsModalState) {
+        case 'minimized':
+          state.settingsModalState = 'visible'
+          return
+        case 'visible':
+          state.settingsModalState = 'minimized'
+          return
+        default:
+          throw new Error('Illegal minimization state of settings modal')
+      }
+    },
+    openSettingsModal (state) {
+      state.settingsModalState = 'visible'
     }
   },
   actions: {
@@ -49,6 +68,15 @@ const interfaceMod = {
     },
     setMobileLayout ({ commit }, value) {
       commit('setMobileLayout', value)
+    },
+    closeSettingsModal ({ commit }) {
+      commit('closeSettingsModal')
+    },
+    openSettingsModal ({ commit }) {
+      commit('openSettingsModal')
+    },
+    togglePeekSettingsModal ({ commit }) {
+      commit('togglePeekSettingsModal')
     }
   }
 }
-- 
cgit v1.2.3-70-g09d2


From bcebec478e43b3851e85c94335940e8fc7546cc8 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 10 May 2020 06:46:06 +0300
Subject: moved stuff from settings, cleaned up naming for tabs, added close
 and peek

---
 src/App.scss                                       |  48 --
 src/components/modal/modal.vue                     |  16 +-
 src/components/settings/settings.js                | 128 ---
 src/components/settings/settings.vue               | 424 ---------
 src/components/settings_modal/settings_modal.js    |  42 +-
 src/components/settings_modal/settings_modal.scss  |  30 +-
 src/components/settings_modal/settings_modal.vue   |  26 +-
 .../settings_modal/tabs/data_import_export.js      |  65 --
 .../settings_modal/tabs/data_import_export.vue     |  43 -
 .../settings_modal/tabs/data_import_export_tab.js  |  65 ++
 .../settings_modal/tabs/data_import_export_tab.vue |  43 +
 .../settings_modal/tabs/filtering_tab.js           |  26 +
 .../settings_modal/tabs/filtering_tab.vue          |  86 ++
 src/components/settings_modal/tabs/general_tab.js  |  32 +
 src/components/settings_modal/tabs/general_tab.vue | 272 ++++++
 .../tabs/helpers/shared_computed_object.js         |  69 ++
 .../settings_modal/tabs/mutes_and_blocks.js        | 124 ---
 .../settings_modal/tabs/mutes_and_blocks.vue       | 173 ----
 .../settings_modal/tabs/mutes_and_blocks_tab.js    | 124 +++
 .../settings_modal/tabs/mutes_and_blocks_tab.vue   | 173 ++++
 .../settings_modal/tabs/notifications.js           |  27 -
 .../settings_modal/tabs/notifications.vue          |  42 -
 .../settings_modal/tabs/notifications_tab.js       |  27 +
 .../settings_modal/tabs/notifications_tab.vue      |  42 +
 src/components/settings_modal/tabs/profile.js      | 179 ----
 src/components/settings_modal/tabs/profile.scss    |  82 --
 src/components/settings_modal/tabs/profile.vue     | 213 -----
 src/components/settings_modal/tabs/profile_tab.js  | 179 ++++
 .../settings_modal/tabs/profile_tab.scss           |  82 ++
 src/components/settings_modal/tabs/profile_tab.vue | 213 +++++
 src/components/settings_modal/tabs/security.js     | 106 ---
 src/components/settings_modal/tabs/security.vue    | 143 ---
 .../settings_modal/tabs/security_tab/confirm.js    |   9 +
 .../settings_modal/tabs/security_tab/confirm.vue   |  22 +
 .../settings_modal/tabs/security_tab/mfa.js        | 155 ++++
 .../settings_modal/tabs/security_tab/mfa.vue       | 174 ++++
 .../tabs/security_tab/mfa_backup_codes.js          |  17 +
 .../tabs/security_tab/mfa_backup_codes.vue         |  35 +
 .../settings_modal/tabs/security_tab/mfa_totp.js   |  49 ++
 .../settings_modal/tabs/security_tab/mfa_totp.vue  |  43 +
 .../tabs/security_tab/security_tab.js              | 106 +++
 .../tabs/security_tab/security_tab.vue             | 143 +++
 .../settings_modal/tabs/theme_tab/preview.vue      | 117 +++
 .../settings_modal/tabs/theme_tab/theme_tab.js     | 759 ++++++++++++++++
 .../settings_modal/tabs/theme_tab/theme_tab.scss   | 339 ++++++++
 .../settings_modal/tabs/theme_tab/theme_tab.vue    | 956 +++++++++++++++++++++
 src/components/settings_modal/tabs/version_tab.js  |  24 +
 src/components/settings_modal/tabs/version_tab.vue |  31 +
 src/components/style_switcher/preview.vue          | 117 ---
 src/components/style_switcher/style_switcher.js    | 758 ----------------
 src/components/style_switcher/style_switcher.scss  | 335 --------
 src/components/style_switcher/style_switcher.vue   | 956 ---------------------
 src/components/tab_switcher/tab_switcher.scss      |   6 +
 src/components/user_card/user_card.vue             |  11 +-
 src/components/user_settings/confirm.js            |   9 -
 src/components/user_settings/confirm.vue           |  22 -
 src/components/user_settings/mfa.js                | 155 ----
 src/components/user_settings/mfa.vue               | 173 ----
 src/components/user_settings/mfa_backup_codes.js   |  17 -
 src/components/user_settings/mfa_backup_codes.vue  |  33 -
 src/components/user_settings/mfa_totp.js           |  49 --
 src/components/user_settings/mfa_totp.vue          |  43 -
 src/components/user_settings/user_settings.js      | 140 ---
 src/components/user_settings/user_settings.vue     | 119 ---
 src/i18n/en.json                                   |   4 +-
 65 files changed, 4509 insertions(+), 4761 deletions(-)
 delete mode 100644 src/components/settings/settings.js
 delete mode 100644 src/components/settings/settings.vue
 delete mode 100644 src/components/settings_modal/tabs/data_import_export.js
 delete mode 100644 src/components/settings_modal/tabs/data_import_export.vue
 create mode 100644 src/components/settings_modal/tabs/data_import_export_tab.js
 create mode 100644 src/components/settings_modal/tabs/data_import_export_tab.vue
 create mode 100644 src/components/settings_modal/tabs/filtering_tab.js
 create mode 100644 src/components/settings_modal/tabs/filtering_tab.vue
 create mode 100644 src/components/settings_modal/tabs/general_tab.js
 create mode 100644 src/components/settings_modal/tabs/general_tab.vue
 create mode 100644 src/components/settings_modal/tabs/helpers/shared_computed_object.js
 delete mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.js
 delete mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.vue
 create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks_tab.js
 create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
 delete mode 100644 src/components/settings_modal/tabs/notifications.js
 delete mode 100644 src/components/settings_modal/tabs/notifications.vue
 create mode 100644 src/components/settings_modal/tabs/notifications_tab.js
 create mode 100644 src/components/settings_modal/tabs/notifications_tab.vue
 delete mode 100644 src/components/settings_modal/tabs/profile.js
 delete mode 100644 src/components/settings_modal/tabs/profile.scss
 delete mode 100644 src/components/settings_modal/tabs/profile.vue
 create mode 100644 src/components/settings_modal/tabs/profile_tab.js
 create mode 100644 src/components/settings_modal/tabs/profile_tab.scss
 create mode 100644 src/components/settings_modal/tabs/profile_tab.vue
 delete mode 100644 src/components/settings_modal/tabs/security.js
 delete mode 100644 src/components/settings_modal/tabs/security.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/confirm.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/confirm.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_totp.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_totp.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/security_tab.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/security_tab.vue
 create mode 100644 src/components/settings_modal/tabs/theme_tab/preview.vue
 create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.js
 create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.scss
 create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.vue
 create mode 100644 src/components/settings_modal/tabs/version_tab.js
 create mode 100644 src/components/settings_modal/tabs/version_tab.vue
 delete mode 100644 src/components/style_switcher/preview.vue
 delete mode 100644 src/components/style_switcher/style_switcher.js
 delete mode 100644 src/components/style_switcher/style_switcher.scss
 delete mode 100644 src/components/style_switcher/style_switcher.vue
 delete mode 100644 src/components/user_settings/confirm.js
 delete mode 100644 src/components/user_settings/confirm.vue
 delete mode 100644 src/components/user_settings/mfa.js
 delete mode 100644 src/components/user_settings/mfa.vue
 delete mode 100644 src/components/user_settings/mfa_backup_codes.js
 delete mode 100644 src/components/user_settings/mfa_backup_codes.vue
 delete mode 100644 src/components/user_settings/mfa_totp.js
 delete mode 100644 src/components/user_settings/mfa_totp.vue
 delete mode 100644 src/components/user_settings/user_settings.js
 delete mode 100644 src/components/user_settings/user_settings.vue

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

diff --git a/src/App.scss b/src/App.scss
index 7db9461c..120eea53 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -860,54 +860,6 @@ nav {
   }
 }
 
-// DELETE
-.setting-item {
-  border-bottom: 2px solid var(--fg, $fallback--fg);
-  margin: 1em 1em 1.4em;
-  padding-bottom: 1.4em;
-
-  > div {
-    margin-bottom: .5em;
-    &:last-child {
-      margin-bottom: 0;
-    }
-  }
-
-  &:last-child {
-    border-bottom: none;
-    padding-bottom: 0;
-    margin-bottom: 1em;
-  }
-
-  select {
-    min-width: 10em;
-  }
-
-
-  textarea {
-    width: 100%;
-    max-width: 100%;
-    height: 100px;
-  }
-
-  .unavailable,
-  .unavailable i {
-    color: var(--cRed, $fallback--cRed);
-    color: $fallback--cRed;
-  }
-
-  .btn {
-    min-height: 28px;
-    min-width: 10em;
-    padding: 0 2em;
-  }
-
-  .number-input {
-    max-width: 6em;
-  }
-}
-// DELETE
-
 .select-multiple {
   display: flex;
   .option-list {
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index cee24241..e5ecc0c0 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -3,6 +3,7 @@
     v-show="isOpen"
     v-body-scroll-lock="isOpen"
     class="modal-view"
+    :class="{ 'modal-background': !noBackground }"
     @click.self="$emit('backdropClicked')"
   >
     <slot />
@@ -15,6 +16,10 @@ export default {
     isOpen: {
       type: Boolean,
       default: true
+    },
+    noBackground: {
+      type: Boolean,
+      default: false
     }
   }
 }
@@ -32,10 +37,19 @@ export default {
   justify-content: center;
   align-items: center;
   overflow: auto;
+  pointer-events: none;
   animation-duration: 0.2s;
-  background-color: rgba(0, 0, 0, 0.5);
   animation-name: modal-background-fadein;
 
+  > * {
+    pointer-events: initial;
+  }
+
+  &.modal-background {
+    pointer-events: initial;
+    background-color: rgba(0, 0, 0, 0.5);
+  }
+
   body:not(.scroll-locked) & {
     opacity: 0;
   }
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
deleted file mode 100644
index 31a9e9be..00000000
--- a/src/components/settings/settings.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/* eslint-env browser */
-import { filter, trim } from 'lodash'
-
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import StyleSwitcher from '../style_switcher/style_switcher.vue'
-import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
-import { extractCommit } from '../../services/version/version.service'
-import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
-import Checkbox from '../checkbox/checkbox.vue'
-
-const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
-const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
-
-const multiChoiceProperties = [
-  'postContentType',
-  'subjectLineBehavior'
-]
-
-const settings = {
-  data () {
-    const instance = this.$store.state.instance
-
-    return {
-      loopSilentAvailable:
-        // Firefox
-        Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
-        // Chrome-likes
-        Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
-        // Future spec, still not supported in Nightly 63 as of 08/2018
-        Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
-
-      backendVersion: instance.backendVersion,
-      frontendVersion: instance.frontendVersion
-    }
-  },
-  components: {
-    TabSwitcher,
-    StyleSwitcher,
-    InterfaceLanguageSwitcher,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    currentSaveStateNotice () {
-      return this.$store.state.interface.settings.currentSaveStateNotice
-    },
-    postFormats () {
-      return this.$store.state.instance.postFormats || []
-    },
-    instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
-    frontendVersionLink () {
-      return pleromaFeCommitUrl + this.frontendVersion
-    },
-    backendVersionLink () {
-      return pleromaBeCommitUrl + extractCommit(this.backendVersion)
-    },
-    // Getting localized values for instance-default properties
-    ...instanceDefaultProperties
-      .filter(key => multiChoiceProperties.includes(key))
-      .map(key => [
-        key + 'DefaultValue',
-        function () {
-          return this.$store.getters.instanceDefaultConfig[key]
-        }
-      ])
-      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
-    ...instanceDefaultProperties
-      .filter(key => !multiChoiceProperties.includes(key))
-      .map(key => [
-        key + 'LocalizedValue',
-        function () {
-          return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
-        }
-      ])
-      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
-    // Generating computed values for vuex properties
-    ...Object.keys(configDefaultState)
-      .map(key => [key, {
-        get () { return this.$store.getters.mergedConfig[key] },
-        set (value) {
-          this.$store.dispatch('setOption', { name: key, value })
-        }
-      }])
-      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
-    // Special cases (need to transform values or perform actions first)
-    muteWordsString: {
-      get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
-      set (value) {
-        this.$store.dispatch('setOption', {
-          name: 'muteWords',
-          value: filter(value.split('\n'), (word) => trim(word).length > 0)
-        })
-      }
-    },
-    useStreamingApi: {
-      get () { return this.$store.getters.mergedConfig.useStreamingApi },
-      set (value) {
-        const promise = value
-          ? this.$store.dispatch('enableMastoSockets')
-          : this.$store.dispatch('disableMastoSockets')
-
-        promise.then(() => {
-          this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
-        }).catch((e) => {
-          console.error('Failed starting MastoAPI Streaming socket', e)
-          this.$store.dispatch('disableMastoSockets')
-          this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
-        })
-      }
-    }
-  },
-  // Updating nested properties
-  watch: {
-    notificationVisibility: {
-      handler (value) {
-        this.$store.dispatch('setOption', {
-          name: 'notificationVisibility',
-          value: this.$store.getters.mergedConfig.notificationVisibility
-        })
-      },
-      deep: true
-    }
-  }
-}
-
-export default settings
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
deleted file mode 100644
index 9e14b449..00000000
--- a/src/components/settings/settings.vue
+++ /dev/null
@@ -1,424 +0,0 @@
-<template>
-  <div class="settings panel panel-default">
-    <div class="panel-heading">
-      <div class="title">
-        {{ $t('settings.settings') }}
-      </div>
-
-      <transition name="fade">
-        <template v-if="currentSaveStateNotice">
-          <div
-            v-if="currentSaveStateNotice.error"
-            class="alert error"
-            @click.prevent
-          >
-            {{ $t('settings.saving_err') }}
-          </div>
-
-          <div
-            v-if="!currentSaveStateNotice.error"
-            class="alert transparent"
-            @click.prevent
-          >
-            {{ $t('settings.saving_ok') }}
-          </div>
-        </template>
-      </transition>
-    </div>
-    <div class="panel-body">
-      <keep-alive>
-        <tab-switcher>
-          <div :label="$t('settings.general')">
-            <div class="setting-item">
-              <h2>{{ $t('settings.interface') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <interface-language-switcher />
-                </li>
-                <li v-if="instanceSpecificPanelPresent">
-                  <Checkbox v-model="hideISP">
-                    {{ $t('settings.hide_isp') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-            <div class="setting-item">
-              <h2>{{ $t('nav.timeline') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="hideMutedPosts">
-                    {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="collapseMessageWithSubject">
-                    {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="streaming">
-                    {{ $t('settings.streaming') }}
-                  </Checkbox>
-                  <ul
-                    class="setting-list suboptions"
-                    :class="[{disabled: !streaming}]"
-                  >
-                    <li>
-                      <Checkbox
-                        v-model="pauseOnUnfocused"
-                        :disabled="!streaming"
-                      >
-                        {{ $t('settings.pause_on_unfocused') }}
-                      </Checkbox>
-                    </li>
-                  </ul>
-                </li>
-                <li>
-                  <Checkbox v-model="useStreamingApi">
-                    {{ $t('settings.useStreamingApi') }}
-                    <br>
-                    <small>
-                      {{ $t('settings.useStreamingApiWarning') }}
-                    </small>
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="autoLoad">
-                    {{ $t('settings.autoload') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="hoverPreview">
-                    {{ $t('settings.reply_link_preview') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="emojiReactionsOnTimeline">
-                    {{ $t('settings.emoji_reactions_on_timeline') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.composing') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="scopeCopy">
-                    {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="alwaysShowSubjectInput">
-                    {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <div>
-                    {{ $t('settings.subject_line_behavior') }}
-                    <label
-                      for="subjectLineBehavior"
-                      class="select"
-                    >
-                      <select
-                        id="subjectLineBehavior"
-                        v-model="subjectLineBehavior"
-                      >
-                        <option value="email">
-                          {{ $t('settings.subject_line_email') }}
-                          {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                        <option value="masto">
-                          {{ $t('settings.subject_line_mastodon') }}
-                          {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                        <option value="noop">
-                          {{ $t('settings.subject_line_noop') }}
-                          {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                      </select>
-                      <i class="icon-down-open" />
-                    </label>
-                  </div>
-                </li>
-                <li v-if="postFormats.length > 0">
-                  <div>
-                    {{ $t('settings.post_status_content_type') }}
-                    <label
-                      for="postContentType"
-                      class="select"
-                    >
-                      <select
-                        id="postContentType"
-                        v-model="postContentType"
-                      >
-                        <option
-                          v-for="postFormat in postFormats"
-                          :key="postFormat"
-                          :value="postFormat"
-                        >
-                          {{ $t(`post_status.content_type["${postFormat}"]`) }}
-                          {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                      </select>
-                      <i class="icon-down-open" />
-                    </label>
-                  </div>
-                </li>
-                <li>
-                  <Checkbox v-model="minimalScopesMode">
-                    {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="autohideFloatingPostButton">
-                    {{ $t('settings.autohide_floating_post_button') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="padEmoji">
-                    {{ $t('settings.pad_emoji') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.attachments') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="hideAttachments">
-                    {{ $t('settings.hide_attachments_in_tl') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="hideAttachmentsInConv">
-                    {{ $t('settings.hide_attachments_in_convo') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <label for="maxThumbnails">
-                    {{ $t('settings.max_thumbnails') }}
-                  </label>
-                  <input
-                    id="maxThumbnails"
-                    v-model.number="maxThumbnails"
-                    class="number-input"
-                    type="number"
-                    min="0"
-                    step="1"
-                  >
-                </li>
-                <li>
-                  <Checkbox v-model="hideNsfw">
-                    {{ $t('settings.nsfw_clickthrough') }}
-                  </Checkbox>
-                </li>
-                <ul class="setting-list suboptions">
-                  <li>
-                    <Checkbox
-                      v-model="preloadImage"
-                      :disabled="!hideNsfw"
-                    >
-                      {{ $t('settings.preload_images') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox
-                      v-model="useOneClickNsfw"
-                      :disabled="!hideNsfw"
-                    >
-                      {{ $t('settings.use_one_click_nsfw') }}
-                    </Checkbox>
-                  </li>
-                </ul>
-                <li>
-                  <Checkbox v-model="stopGifs">
-                    {{ $t('settings.stop_gifs') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="loopVideo">
-                    {{ $t('settings.loop_video') }}
-                  </Checkbox>
-                  <ul
-                    class="setting-list suboptions"
-                    :class="[{disabled: !streaming}]"
-                  >
-                    <li>
-                      <Checkbox
-                        v-model="loopVideoSilentOnly"
-                        :disabled="!loopVideo || !loopSilentAvailable"
-                      >
-                        {{ $t('settings.loop_video_silent_only') }}
-                      </Checkbox>
-                      <div
-                        v-if="!loopSilentAvailable"
-                        class="unavailable"
-                      >
-                        <i class="icon-globe" />! {{ $t('settings.limited_availability') }}
-                      </div>
-                    </li>
-                  </ul>
-                </li>
-                <li>
-                  <Checkbox v-model="playVideosInModal">
-                    {{ $t('settings.play_videos_in_modal') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="useContainFit">
-                    {{ $t('settings.use_contain_fit') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.notifications') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="webPushNotifications">
-                    {{ $t('settings.enable_web_push_notifications') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.fun') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="greentext">
-                    {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-          </div>
-
-          <div :label="$t('settings.theme')">
-            <div class="setting-item">
-              <style-switcher />
-            </div>
-          </div>
-
-          <div :label="$t('settings.filtering')">
-            <div class="setting-item">
-              <div class="select-multiple">
-                <span class="label">{{ $t('settings.notification_visibility') }}</span>
-                <ul class="option-list">
-                  <li>
-                    <Checkbox v-model="notificationVisibility.likes">
-                      {{ $t('settings.notification_visibility_likes') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.repeats">
-                      {{ $t('settings.notification_visibility_repeats') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.follows">
-                      {{ $t('settings.notification_visibility_follows') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.mentions">
-                      {{ $t('settings.notification_visibility_mentions') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.moves">
-                      {{ $t('settings.notification_visibility_moves') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.emojiReactions">
-                      {{ $t('settings.notification_visibility_emoji_reactions') }}
-                    </Checkbox>
-                  </li>
-                </ul>
-              </div>
-              <div>
-                {{ $t('settings.replies_in_timeline') }}
-                <label
-                  for="replyVisibility"
-                  class="select"
-                >
-                  <select
-                    id="replyVisibility"
-                    v-model="replyVisibility"
-                  >
-                    <option
-                      value="all"
-                      selected
-                    >{{ $t('settings.reply_visibility_all') }}</option>
-                    <option value="following">{{ $t('settings.reply_visibility_following') }}</option>
-                    <option value="self">{{ $t('settings.reply_visibility_self') }}</option>
-                  </select>
-                  <i class="icon-down-open" />
-                </label>
-              </div>
-              <div>
-                <Checkbox v-model="hidePostStats">
-                  {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
-                </Checkbox>
-              </div>
-              <div>
-                <Checkbox v-model="hideUserStats">
-                  {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
-                </Checkbox>
-              </div>
-            </div>
-            <div class="setting-item">
-              <div>
-                <p>{{ $t('settings.filtering_explanation') }}</p>
-                <textarea
-                  id="muteWords"
-                  v-model="muteWordsString"
-                />
-              </div>
-              <div>
-                <Checkbox v-model="hideFilteredStatuses">
-                  {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
-                </Checkbox>
-              </div>
-            </div>
-          </div>
-          <div :label="$t('settings.version.title')">
-            <div class="setting-item">
-              <ul class="setting-list">
-                <li>
-                  <p>{{ $t('settings.version.backend_version') }}</p>
-                  <ul class="option-list">
-                    <li>
-                      <a
-                        :href="backendVersionLink"
-                        target="_blank"
-                      >{{ backendVersion }}</a>
-                    </li>
-                  </ul>
-                </li>
-                <li>
-                  <p>{{ $t('settings.version.frontend_version') }}</p>
-                  <ul class="option-list">
-                    <li>
-                      <a
-                        :href="frontendVersionLink"
-                        target="_blank"
-                      >{{ frontendVersion }}</a>
-                    </li>
-                  </ul>
-                </li>
-              </ul>
-            </div>
-          </div>
-        </tab-switcher>
-      </keep-alive>
-    </div>
-  </div>
-</template>
-
-<script src="./settings.js">
-</script>
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 1f4c038f..d60babf6 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -1,21 +1,30 @@
-import Modal from '../modal/modal.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
+import Modal from 'src/components/modal/modal.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
 
-import Profile from './tabs/profile.vue'
-import Security from './tabs/security.vue'
-import Notifications from './tabs/notifications.vue'
-import DataImportExport from './tabs/data_import_export.vue'
-import MutesAndBlocks from './tabs/mutes_and_blocks.vue'
+import DataImportExportTab from './tabs/data_import_export_tab.vue'
+import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
+import NotificationsTab from './tabs/notifications_tab.vue'
+import FilteringTab from './tabs/filtering_tab.vue'
+import SecurityTab from './tabs/security_tab/security_tab.vue'
+import ProfileTab from './tabs/profile_tab.vue'
+import GeneralTab from './tabs/general_tab.vue'
+import VersionTab from './tabs/version_tab.vue'
+import ThemeTab from './tabs/theme_tab/theme_tab.vue'
 
 const SettingsModal = {
   components: {
     Modal,
     TabSwitcher,
-    Profile,
-    Security,
-    Notifications,
-    DataImportExport,
-    MutesAndBlocks
+
+    DataImportExportTab,
+    MutesAndBlocksTab,
+    NotificationsTab,
+    FilteringTab,
+    SecurityTab,
+    ProfileTab,
+    GeneralTab,
+    VersionTab,
+    ThemeTab
   },
   data () {
     return {
@@ -28,11 +37,20 @@ const SettingsModal = {
     },
     modalActivated () {
       return this.$store.state.interface.settingsModalState !== 'hidden'
+    },
+    modalPeeked () {
+      return this.$store.state.interface.settingsModalState === 'minimized'
     }
   },
   watch: {
   },
   methods: {
+    closeModal () {
+      this.$store.dispatch('closeSettingsModal')
+    },
+    peekModal () {
+      this.$store.dispatch('togglePeekSettingsModal')
+    }
   }
 }
 
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 8cea52d2..3efbe205 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -1,14 +1,29 @@
-@import '../../_variables.scss';
+@import 'src/_variables.scss';
 .settings-modal {
+
   .settings_tab-switcher {
     height: 100%;
   }
+  &.peek {
+    .settings-modal-panel {
+      transform: translateY(calc(100% - 50px));
+    }
+  }
+
   .settings-modal-panel {
+    transition: transform;
+    transition-timing-function: ease-in-out;
+    transition-duration: 300ms;
     width: 1000px;
     max-width: 90vw;
     height: 90vh;
+    @media all and (max-width: 800px) {
+      max-width: 100vw;
+      height: 100vh;
+    }
   }
   .panel-body {
+    height: 100%;
     overflow-y: hidden;
   }
   .setting-item {
@@ -16,6 +31,12 @@
     margin: 1em 1em 1.4em;
     padding-bottom: 1.4em;
 
+    .btn {
+      min-height: 28px;
+      min-width: 10em;
+      padding: 0 2em;
+    }
+
     > div {
       margin-bottom: .5em;
       &:last-child {
@@ -33,7 +54,6 @@
       min-width: 10em;
     }
 
-
     textarea {
       width: 100%;
       max-width: 100%;
@@ -46,12 +66,6 @@
       color: $fallback--cRed;
     }
 
-    .btn {
-      min-height: 28px;
-      min-width: 10em;
-      padding: 0 2em;
-    }
-
     .number-input {
       max-width: 6em;
     }
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 9e35d3f6..53481bdd 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -3,10 +3,20 @@
   v-if="isLoggedIn && !resettingForm"
   :is-open="modalActivated"
   class="settings-modal"
+  :class="{ peek: modalPeeked }"
+  :no-background="modalPeeked"
   >
   <div class="settings-modal-panel panel">
     <div class="panel-heading">
-      {{ $t('settings.settings') }}
+      <span class="title">
+        {{ $t('settings.settings') }}
+      </span>
+      <button class="btn" @click="peekModal">
+        {{ $t('general.peek') }}
+      </button>
+      <button class="btn" @click="closeModal">
+        {{ $t('general.close') }}
+      </button>
     </div>
     <div class="panel-body">
       <tab-switcher
@@ -15,11 +25,15 @@
         :scrollableTabs="true"
         ref="tabSwitcher"
         >
-        <div :label="$t('settings.profile_tab')"><Profile /></div>
-        <div :label="$t('settings.security_tab')"><Security /></div>
-        <div :label="$t('settings.notifications')"><Notifications /></div>
-        <div :label="$t('settings.data_import_export_tab')"><DataImportExport /></div>
-        <div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocks /></div>
+        <div :label="$t('settings.general')"><GeneralTab /></div>
+        <div :label="$t('settings.profile_tab')"><ProfileTab /></div>
+        <div :label="$t('settings.security_tab')"><SecurityTab /></div>
+        <div :label="$t('settings.filtering')"><FilteringTab /></div>
+        <div :label="$t('settings.theme')"><ThemeTab /></div>
+        <div :label="$t('settings.notifications')"><NotificationsTab /></div>
+        <div :label="$t('settings.data_import_export_tab')"><DataImportExportTab /></div>
+        <div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocksTab /></div>
+        <div :label="$t('settings.version.title')"><VersionTab /></div>
       </tab-switcher>
     </div>
   </div>
diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js
deleted file mode 100644
index f68d12e9..00000000
--- a/src/components/settings_modal/tabs/data_import_export.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import Importer from '../../importer/importer.vue'
-import Exporter from '../../exporter/exporter.vue'
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const DataImportExport = {
-  data () {
-    return {
-      activeTab: 'profile',
-      newDomainToMute: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    Importer,
-    Exporter,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    }
-  },
-  methods: {
-    getFollowsContent () {
-      return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
-        .then(this.generateExportableUsersContent)
-    },
-    getBlocksContent () {
-      return this.$store.state.api.backendInteractor.fetchBlocks()
-        .then(this.generateExportableUsersContent)
-    },
-    importFollows (file) {
-      return this.$store.state.api.backendInteractor.importFollows({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    importBlocks (file) {
-      return this.$store.state.api.backendInteractor.importBlocks({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    generateExportableUsersContent (users) {
-      // Get addresses
-      return users.map((user) => {
-        // check is it's a local user
-        if (user && user.is_local) {
-          // append the instance address
-          // eslint-disable-next-line no-undef
-          return user.screen_name + '@' + location.hostname
-        }
-        return user.screen_name
-      }).join('\n')
-    }
-  }
-}
-
-export default DataImportExport
diff --git a/src/components/settings_modal/tabs/data_import_export.vue b/src/components/settings_modal/tabs/data_import_export.vue
deleted file mode 100644
index 464df6d3..00000000
--- a/src/components/settings_modal/tabs/data_import_export.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<div
-  :label="$t('settings.data_import_export_tab')"
-  >
-  <div class="setting-item">
-    <h2>{{ $t('settings.follow_import') }}</h2>
-    <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
-    <Importer
-      :submit-handler="importFollows"
-      :success-message="$t('settings.follows_imported')"
-      :error-message="$t('settings.follow_import_error')"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.follow_export') }}</h2>
-    <Exporter
-      :get-content="getFollowsContent"
-      filename="friends.csv"
-      :export-button-label="$t('settings.follow_export_button')"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.block_import') }}</h2>
-    <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
-    <Importer
-      :submit-handler="importBlocks"
-      :success-message="$t('settings.blocks_imported')"
-      :error-message="$t('settings.block_import_error')"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.block_export') }}</h2>
-    <Exporter
-      :get-content="getBlocksContent"
-      filename="blocks.csv"
-      :export-button-label="$t('settings.block_export_button')"
-      />
-  </div>
-</div>
-</template>
-
-<script src="./data_import_export.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js
new file mode 100644
index 00000000..168f89e1
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -0,0 +1,65 @@
+import Importer from 'src/components/importer/importer.vue'
+import Exporter from 'src/components/exporter/exporter.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const DataImportExportTab = {
+  data () {
+    return {
+      activeTab: 'profile',
+      newDomainToMute: ''
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    Importer,
+    Exporter,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
+  methods: {
+    getFollowsContent () {
+      return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+        .then(this.generateExportableUsersContent)
+    },
+    getBlocksContent () {
+      return this.$store.state.api.backendInteractor.fetchBlocks()
+        .then(this.generateExportableUsersContent)
+    },
+    importFollows (file) {
+      return this.$store.state.api.backendInteractor.importFollows({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    importBlocks (file) {
+      return this.$store.state.api.backendInteractor.importBlocks({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    generateExportableUsersContent (users) {
+      // Get addresses
+      return users.map((user) => {
+        // check is it's a local user
+        if (user && user.is_local) {
+          // append the instance address
+          // eslint-disable-next-line no-undef
+          return user.screen_name + '@' + location.hostname
+        }
+        return user.screen_name
+      }).join('\n')
+    }
+  }
+}
+
+export default DataImportExportTab
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue
new file mode 100644
index 00000000..3ddc8b03
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -0,0 +1,43 @@
+<template>
+<div
+  :label="$t('settings.data_import_export_tab')"
+  >
+  <div class="setting-item">
+    <h2>{{ $t('settings.follow_import') }}</h2>
+    <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
+    <Importer
+      :submit-handler="importFollows"
+      :success-message="$t('settings.follows_imported')"
+      :error-message="$t('settings.follow_import_error')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.follow_export') }}</h2>
+    <Exporter
+      :get-content="getFollowsContent"
+      filename="friends.csv"
+      :export-button-label="$t('settings.follow_export_button')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.block_import') }}</h2>
+    <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
+    <Importer
+      :submit-handler="importBlocks"
+      :success-message="$t('settings.blocks_imported')"
+      :error-message="$t('settings.block_import_error')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.block_export') }}</h2>
+    <Exporter
+      :get-content="getBlocksContent"
+      filename="blocks.csv"
+      :export-button-label="$t('settings.block_export_button')"
+      />
+  </div>
+</div>
+</template>
+
+<script src="./data_import_export_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
new file mode 100644
index 00000000..ec330667
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -0,0 +1,26 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import SharedComputedObject from './helpers/shared_computed_object.js'
+
+const FilteringTab = {
+  components: {
+    Checkbox
+  },
+  computed: {
+    ...SharedComputedObject()
+  },
+  // Updating nested properties
+  watch: {
+    notificationVisibility: {
+      handler (value) {
+        this.$store.dispatch('setOption', {
+          name: 'notificationVisibility',
+          value: this.$store.getters.mergedConfig.notificationVisibility
+        })
+      },
+      deep: true
+    }
+  }
+}
+
+export default FilteringTab
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
new file mode 100644
index 00000000..647ec7b4
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -0,0 +1,86 @@
+<template>
+<div :label="$t('settings.filtering')">
+  <div class="setting-item">
+    <div class="select-multiple">
+      <span class="label">{{ $t('settings.notification_visibility') }}</span>
+      <ul class="option-list">
+        <li>
+          <Checkbox v-model="notificationVisibility.likes">
+            {{ $t('settings.notification_visibility_likes') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.repeats">
+            {{ $t('settings.notification_visibility_repeats') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.follows">
+            {{ $t('settings.notification_visibility_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.mentions">
+            {{ $t('settings.notification_visibility_mentions') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.moves">
+            {{ $t('settings.notification_visibility_moves') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.emojiReactions">
+            {{ $t('settings.notification_visibility_emoji_reactions') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+    <div>
+      {{ $t('settings.replies_in_timeline') }}
+      <label
+        for="replyVisibility"
+        class="select"
+        >
+        <select
+          id="replyVisibility"
+          v-model="replyVisibility"
+          >
+          <option
+            value="all"
+            selected
+            >{{ $t('settings.reply_visibility_all') }}</option>
+          <option value="following">{{ $t('settings.reply_visibility_following') }}</option>
+          <option value="self">{{ $t('settings.reply_visibility_self') }}</option>
+        </select>
+        <i class="icon-down-open" />
+      </label>
+    </div>
+    <div>
+      <Checkbox v-model="hidePostStats">
+        {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
+      </Checkbox>
+    </div>
+    <div>
+      <Checkbox v-model="hideUserStats">
+        {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
+      </Checkbox>
+    </div>
+  </div>
+  <div class="setting-item">
+    <div>
+      <p>{{ $t('settings.filtering_explanation') }}</p>
+      <textarea
+        id="muteWords"
+        v-model="muteWordsString"
+        />
+    </div>
+    <div>
+      <Checkbox v-model="hideFilteredStatuses">
+        {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
+      </Checkbox>
+    </div>
+  </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
new file mode 100644
index 00000000..82bf6862
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -0,0 +1,32 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+
+import SharedComputedObject from './helpers/shared_computed_object.js'
+
+const GeneralTab = {
+  data () {
+    const instance = this.$store.state.instance
+    return {
+      loopSilentAvailable:
+      // Firefox
+      Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
+      // Chrome-likes
+      Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
+      // Future spec, still not supported in Nightly 63 as of 08/2018
+      Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
+    }
+  },
+  components: {
+    Checkbox,
+    InterfaceLanguageSwitcher
+  },
+  computed: {
+    postFormats () {
+      return this.$store.state.instance.postFormats || []
+    },
+    instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
+    ...SharedComputedObject()
+  }
+}
+
+export default GeneralTab
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
new file mode 100644
index 00000000..0d2da07a
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -0,0 +1,272 @@
+<template>
+<div :label="$t('settings.general')">
+  <div class="setting-item">
+    <h2>{{ $t('settings.interface') }}</h2>
+    <ul class="setting-list">
+      <li>
+        <interface-language-switcher />
+      </li>
+      <li v-if="instanceSpecificPanelPresent">
+        <Checkbox v-model="hideISP">
+          {{ $t('settings.hide_isp') }}
+        </Checkbox>
+      </li>
+    </ul>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('nav.timeline') }}</h2>
+    <ul class="setting-list">
+      <li>
+        <Checkbox v-model="hideMutedPosts">
+          {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="collapseMessageWithSubject">
+          {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="streaming">
+          {{ $t('settings.streaming') }}
+        </Checkbox>
+        <ul
+          class="setting-list suboptions"
+          :class="[{disabled: !streaming}]"
+          >
+          <li>
+            <Checkbox
+              v-model="pauseOnUnfocused"
+              :disabled="!streaming"
+              >
+              {{ $t('settings.pause_on_unfocused') }}
+            </Checkbox>
+          </li>
+        </ul>
+      </li>
+      <li>
+        <Checkbox v-model="useStreamingApi">
+          {{ $t('settings.useStreamingApi') }}
+          <br>
+          <small>
+            {{ $t('settings.useStreamingApiWarning') }}
+          </small>
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="autoLoad">
+          {{ $t('settings.autoload') }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="hoverPreview">
+          {{ $t('settings.reply_link_preview') }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="emojiReactionsOnTimeline">
+          {{ $t('settings.emoji_reactions_on_timeline') }}
+        </Checkbox>
+      </li>
+    </ul>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.composing') }}</h2>
+    <ul class="setting-list">
+      <li>
+        <Checkbox v-model="scopeCopy">
+          {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="alwaysShowSubjectInput">
+          {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <div>
+          {{ $t('settings.subject_line_behavior') }}
+          <label
+            for="subjectLineBehavior"
+            class="select"
+            >
+            <select
+              id="subjectLineBehavior"
+              v-model="subjectLineBehavior"
+              >
+              <option value="email">
+                {{ $t('settings.subject_line_email') }}
+                {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
+              </option>
+              <option value="masto">
+                {{ $t('settings.subject_line_mastodon') }}
+                {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
+              </option>
+              <option value="noop">
+                {{ $t('settings.subject_line_noop') }}
+                {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
+              </option>
+            </select>
+            <i class="icon-down-open" />
+          </label>
+        </div>
+      </li>
+      <li v-if="postFormats.length > 0">
+        <div>
+          {{ $t('settings.post_status_content_type') }}
+          <label
+            for="postContentType"
+            class="select"
+            >
+            <select
+              id="postContentType"
+              v-model="postContentType"
+              >
+              <option
+                v-for="postFormat in postFormats"
+                :key="postFormat"
+                :value="postFormat"
+                >
+                {{ $t(`post_status.content_type["${postFormat}"]`) }}
+                {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
+              </option>
+            </select>
+            <i class="icon-down-open" />
+          </label>
+        </div>
+      </li>
+      <li>
+        <Checkbox v-model="minimalScopesMode">
+          {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="autohideFloatingPostButton">
+          {{ $t('settings.autohide_floating_post_button') }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="padEmoji">
+          {{ $t('settings.pad_emoji') }}
+        </Checkbox>
+      </li>
+    </ul>
+  </div>
+
+  <div class="setting-item">
+      <h2>{{ $t('settings.attachments') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <Checkbox v-model="hideAttachments">
+            {{ $t('settings.hide_attachments_in_tl') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="hideAttachmentsInConv">
+            {{ $t('settings.hide_attachments_in_convo') }}
+          </Checkbox>
+        </li>
+        <li>
+          <label for="maxThumbnails">
+            {{ $t('settings.max_thumbnails') }}
+          </label>
+          <input
+            id="maxThumbnails"
+            v-model.number="maxThumbnails"
+            class="number-input"
+            type="number"
+            min="0"
+            step="1"
+            >
+        </li>
+        <li>
+          <Checkbox v-model="hideNsfw">
+            {{ $t('settings.nsfw_clickthrough') }}
+          </Checkbox>
+        </li>
+        <ul class="setting-list suboptions">
+          <li>
+            <Checkbox
+              v-model="preloadImage"
+              :disabled="!hideNsfw"
+              >
+              {{ $t('settings.preload_images') }}
+            </Checkbox>
+          </li>
+          <li>
+            <Checkbox
+              v-model="useOneClickNsfw"
+              :disabled="!hideNsfw"
+              >
+              {{ $t('settings.use_one_click_nsfw') }}
+            </Checkbox>
+          </li>
+        </ul>
+        <li>
+          <Checkbox v-model="stopGifs">
+            {{ $t('settings.stop_gifs') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="loopVideo">
+            {{ $t('settings.loop_video') }}
+          </Checkbox>
+          <ul
+            class="setting-list suboptions"
+            :class="[{disabled: !streaming}]"
+            >
+            <li>
+              <Checkbox
+                v-model="loopVideoSilentOnly"
+                :disabled="!loopVideo || !loopSilentAvailable"
+                >
+                {{ $t('settings.loop_video_silent_only') }}
+              </Checkbox>
+              <div
+                v-if="!loopSilentAvailable"
+                class="unavailable"
+                >
+                <i class="icon-globe" />! {{ $t('settings.limited_availability') }}
+              </div>
+            </li>
+          </ul>
+        </li>
+        <li>
+          <Checkbox v-model="playVideosInModal">
+            {{ $t('settings.play_videos_in_modal') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="useContainFit">
+            {{ $t('settings.use_contain_fit') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+
+    <div class="setting-item">
+      <h2>{{ $t('settings.notifications') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <Checkbox v-model="webPushNotifications">
+            {{ $t('settings.enable_web_push_notifications') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+
+    <div class="setting-item">
+      <h2>{{ $t('settings.fun') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <Checkbox v-model="greentext">
+            {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script src="./general_tab.js"></script>
diff --git a/src/components/settings_modal/tabs/helpers/shared_computed_object.js b/src/components/settings_modal/tabs/helpers/shared_computed_object.js
new file mode 100644
index 00000000..61643e3b
--- /dev/null
+++ b/src/components/settings_modal/tabs/helpers/shared_computed_object.js
@@ -0,0 +1,69 @@
+import { filter, trim } from 'lodash'
+import { instanceDefaultProperties, defaultState as configDefaultState } from 'src/modules/config.js'
+
+const multiChoiceProperties = [
+  'postContentType',
+  'subjectLineBehavior'
+]
+
+const SharedComputedObject = () => ({
+  user () {
+    return this.$store.state.users.currentUser
+  },
+  // Getting localized values for instance-default properties
+  ...instanceDefaultProperties
+    .filter(key => multiChoiceProperties.includes(key))
+    .map(key => [
+      key + 'DefaultValue',
+      function () {
+        return this.$store.getters.instanceDefaultConfig[key]
+      }
+    ])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  ...instanceDefaultProperties
+    .filter(key => !multiChoiceProperties.includes(key))
+    .map(key => [
+      key + 'LocalizedValue',
+      function () {
+        return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
+      }
+    ])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  // Generating computed values for vuex properties
+  ...Object.keys(configDefaultState)
+    .map(key => [key, {
+      get () { return this.$store.getters.mergedConfig[key] },
+      set (value) {
+        this.$store.dispatch('setOption', { name: key, value })
+      }
+    }])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  // Special cases (need to transform values or perform actions first)
+  muteWordsString: {
+    get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
+    set (value) {
+      this.$store.dispatch('setOption', {
+        name: 'muteWords',
+        value: filter(value.split('\n'), (word) => trim(word).length > 0)
+      })
+    }
+  },
+  useStreamingApi: {
+    get () { return this.$store.getters.mergedConfig.useStreamingApi },
+    set (value) {
+      const promise = value
+        ? this.$store.dispatch('enableMastoSockets')
+        : this.$store.dispatch('disableMastoSockets')
+
+      promise.then(() => {
+        this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
+      }).catch((e) => {
+        console.error('Failed starting MastoAPI Streaming socket', e)
+        this.$store.dispatch('disableMastoSockets')
+        this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
+      })
+    }
+  }
+})
+
+export default SharedComputedObject
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js
deleted file mode 100644
index 51895ddc..00000000
--- a/src/components/settings_modal/tabs/mutes_and_blocks.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import get from 'lodash/get'
-import map from 'lodash/map'
-import reject from 'lodash/reject'
-import Autosuggest from '../../autosuggest/autosuggest.vue'
-import TabSwitcher from '../../tab_switcher/tab_switcher.js'
-import BlockCard from '../../block_card/block_card.vue'
-import MuteCard from '../../mute_card/mute_card.vue'
-import DomainMuteCard from '../../domain_mute_card/domain_mute_card.vue'
-import SelectableList from '../../selectable_list/selectable_list.vue'
-import ProgressButton from '../../progress_button/progress_button.vue'
-import withSubscription from '../../../hocs/with_subscription/with_subscription'
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const BlockList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
-  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const MuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const DomainMuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const MutesAndBlocks = {
-  data () {
-    return {
-      activeTab: 'profile',
-      newDomainToMute: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    TabSwitcher,
-    BlockList,
-    MuteList,
-    DomainMuteList,
-    BlockCard,
-    MuteCard,
-    DomainMuteCard,
-    ProgressButton,
-    Autosuggest,
-    Checkbox
-  },
-  methods: {
-    importFollows (file) {
-      return this.$store.state.api.backendInteractor.importFollows({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    importBlocks (file) {
-      return this.$store.state.api.backendInteractor.importBlocks({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    generateExportableUsersContent (users) {
-      // Get addresses
-      return users.map((user) => {
-        // check is it's a local user
-        if (user && user.is_local) {
-          // append the instance address
-          // eslint-disable-next-line no-undef
-          return user.screen_name + '@' + location.hostname
-        }
-        return user.screen_name
-      }).join('\n')
-    },
-    activateTab (tabName) {
-      this.activeTab = tabName
-    },
-    filterUnblockedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    filterUnMutedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    queryUserIds (query) {
-      return this.$store.dispatch('searchUsers', query)
-        .then((users) => map(users, 'id'))
-    },
-    blockUsers (ids) {
-      return this.$store.dispatch('blockUsers', ids)
-    },
-    unblockUsers (ids) {
-      return this.$store.dispatch('unblockUsers', ids)
-    },
-    muteUsers (ids) {
-      return this.$store.dispatch('muteUsers', ids)
-    },
-    unmuteUsers (ids) {
-      return this.$store.dispatch('unmuteUsers', ids)
-    },
-    unmuteDomains (domains) {
-      return this.$store.dispatch('unmuteDomains', domains)
-    },
-    muteDomain () {
-      return this.$store.dispatch('muteDomain', this.newDomainToMute)
-        .then(() => { this.newDomainToMute = '' })
-    }
-  }
-}
-
-export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.vue b/src/components/settings_modal/tabs/mutes_and_blocks.vue
deleted file mode 100644
index 3aff47a0..00000000
--- a/src/components/settings_modal/tabs/mutes_and_blocks.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<template>
-  <tab-switcher>
-    <div :label="$t('settings.blocks_tab')">
-      <div class="profile-edit-usersearch-wrapper">
-        <Autosuggest
-          :filter="filterUnblockedUsers"
-          :query="queryUserIds"
-          :placeholder="$t('settings.search_user_to_block')"
-          >
-          <BlockCard
-            slot-scope="row"
-            :user-id="row.item"
-            />
-        </Autosuggest>
-      </div>
-      <BlockList
-        :refresh="true"
-        :get-key="identity"
-        >
-        <template
-          slot="header"
-          slot-scope="{selected}"
-          >
-          <div class="profile-edit-bulk-actions">
-            <ProgressButton
-              v-if="selected.length > 0"
-              class="btn btn-default"
-              :click="() => blockUsers(selected)"
-              >
-              {{ $t('user_card.block') }}
-              <template slot="progress">
-                {{ $t('user_card.block_progress') }}
-              </template>
-            </ProgressButton>
-            <ProgressButton
-              v-if="selected.length > 0"
-              class="btn btn-default"
-              :click="() => unblockUsers(selected)"
-              >
-              {{ $t('user_card.unblock') }}
-              <template slot="progress">
-                {{ $t('user_card.unblock_progress') }}
-              </template>
-            </ProgressButton>
-          </div>
-        </template>
-        <template
-          slot="item"
-          slot-scope="{item}"
-          >
-          <BlockCard :user-id="item" />
-        </template>
-        <template slot="empty">
-          {{ $t('settings.no_blocks') }}
-        </template>
-      </BlockList>
-    </div>
-
-    <div :label="$t('settings.mutes_tab')">
-      <tab-switcher>
-        <div label="Users">
-          <div class="profile-edit-usersearch-wrapper">
-            <Autosuggest
-              :filter="filterUnMutedUsers"
-              :query="queryUserIds"
-              :placeholder="$t('settings.search_user_to_mute')"
-              >
-              <MuteCard
-                slot-scope="row"
-                :user-id="row.item"
-                />
-            </Autosuggest>
-          </div>
-          <MuteList
-            :refresh="true"
-            :get-key="identity"
-            >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-              >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => muteUsers(selected)"
-                  >
-                  {{ $t('user_card.mute') }}
-                  <template slot="progress">
-                    {{ $t('user_card.mute_progress') }}
-                  </template>
-                </ProgressButton>
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => unmuteUsers(selected)"
-                  >
-                  {{ $t('user_card.unmute') }}
-                  <template slot="progress">
-                    {{ $t('user_card.unmute_progress') }}
-                  </template>
-                </ProgressButton>
-              </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-              >
-              <MuteCard :user-id="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_mutes') }}
-            </template>
-          </MuteList>
-        </div>
-
-        <div :label="$t('settings.domain_mutes')">
-          <div class="profile-edit-domain-mute-form">
-            <input
-              v-model="newDomainToMute"
-              :placeholder="$t('settings.type_domains_to_mute')"
-              type="text"
-              @keyup.enter="muteDomain"
-              >
-            <ProgressButton
-              class="btn btn-default"
-              :click="muteDomain"
-              >
-              {{ $t('domain_mute_card.mute') }}
-              <template slot="progress">
-                {{ $t('domain_mute_card.mute_progress') }}
-              </template>
-            </ProgressButton>
-          </div>
-          <DomainMuteList
-            :refresh="true"
-            :get-key="identity"
-            >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-              >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => unmuteDomains(selected)"
-                  >
-                  {{ $t('domain_mute_card.unmute') }}
-                  <template slot="progress">
-                    {{ $t('domain_mute_card.unmute_progress') }}
-                  </template>
-                </ProgressButton>
-              </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-              >
-              <DomainMuteCard :domain="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_mutes') }}
-            </template>
-          </DomainMuteList>
-        </div>
-      </tab-switcher>
-    </div>
-  </tab-switcher>
-</template>
-
-<script src="./mutes_and_blocks.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
new file mode 100644
index 00000000..3f6b7205
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -0,0 +1,124 @@
+import get from 'lodash/get'
+import map from 'lodash/map'
+import reject from 'lodash/reject'
+import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import BlockCard from 'src/components/block_card/block_card.vue'
+import MuteCard from 'src/components/mute_card/mute_card.vue'
+import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
+import SelectableList from 'src/components/selectable_list/selectable_list.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const BlockList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
+  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const MuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const DomainMuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const MutesAndBlocks = {
+  data () {
+    return {
+      activeTab: 'profile',
+      newDomainToMute: ''
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    TabSwitcher,
+    BlockList,
+    MuteList,
+    DomainMuteList,
+    BlockCard,
+    MuteCard,
+    DomainMuteCard,
+    ProgressButton,
+    Autosuggest,
+    Checkbox
+  },
+  methods: {
+    importFollows (file) {
+      return this.$store.state.api.backendInteractor.importFollows({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    importBlocks (file) {
+      return this.$store.state.api.backendInteractor.importBlocks({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    generateExportableUsersContent (users) {
+      // Get addresses
+      return users.map((user) => {
+        // check is it's a local user
+        if (user && user.is_local) {
+          // append the instance address
+          // eslint-disable-next-line no-undef
+          return user.screen_name + '@' + location.hostname
+        }
+        return user.screen_name
+      }).join('\n')
+    },
+    activateTab (tabName) {
+      this.activeTab = tabName
+    },
+    filterUnblockedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const user = this.$store.getters.findUser(userId)
+        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
+      })
+    },
+    filterUnMutedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const user = this.$store.getters.findUser(userId)
+        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
+      })
+    },
+    queryUserIds (query) {
+      return this.$store.dispatch('searchUsers', query)
+        .then((users) => map(users, 'id'))
+    },
+    blockUsers (ids) {
+      return this.$store.dispatch('blockUsers', ids)
+    },
+    unblockUsers (ids) {
+      return this.$store.dispatch('unblockUsers', ids)
+    },
+    muteUsers (ids) {
+      return this.$store.dispatch('muteUsers', ids)
+    },
+    unmuteUsers (ids) {
+      return this.$store.dispatch('unmuteUsers', ids)
+    },
+    unmuteDomains (domains) {
+      return this.$store.dispatch('unmuteDomains', domains)
+    },
+    muteDomain () {
+      return this.$store.dispatch('muteDomain', this.newDomainToMute)
+        .then(() => { this.newDomainToMute = '' })
+    }
+  }
+}
+
+export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
new file mode 100644
index 00000000..7fce7b78
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -0,0 +1,173 @@
+<template>
+  <tab-switcher>
+    <div :label="$t('settings.blocks_tab')">
+      <div class="profile-edit-usersearch-wrapper">
+        <Autosuggest
+          :filter="filterUnblockedUsers"
+          :query="queryUserIds"
+          :placeholder="$t('settings.search_user_to_block')"
+          >
+          <BlockCard
+            slot-scope="row"
+            :user-id="row.item"
+            />
+        </Autosuggest>
+      </div>
+      <BlockList
+        :refresh="true"
+        :get-key="i => i"
+        >
+        <template
+          slot="header"
+          slot-scope="{selected}"
+          >
+          <div class="profile-edit-bulk-actions">
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default"
+              :click="() => blockUsers(selected)"
+              >
+              {{ $t('user_card.block') }}
+              <template slot="progress">
+                {{ $t('user_card.block_progress') }}
+              </template>
+            </ProgressButton>
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default"
+              :click="() => unblockUsers(selected)"
+              >
+              {{ $t('user_card.unblock') }}
+              <template slot="progress">
+                {{ $t('user_card.unblock_progress') }}
+              </template>
+            </ProgressButton>
+          </div>
+        </template>
+        <template
+          slot="item"
+          slot-scope="{item}"
+          >
+          <BlockCard :user-id="item" />
+        </template>
+        <template slot="empty">
+          {{ $t('settings.no_blocks') }}
+        </template>
+      </BlockList>
+    </div>
+
+    <div :label="$t('settings.mutes_tab')">
+      <tab-switcher>
+        <div label="Users">
+          <div class="profile-edit-usersearch-wrapper">
+            <Autosuggest
+              :filter="filterUnMutedUsers"
+              :query="queryUserIds"
+              :placeholder="$t('settings.search_user_to_mute')"
+              >
+              <MuteCard
+                slot-scope="row"
+                :user-id="row.item"
+                />
+            </Autosuggest>
+          </div>
+          <MuteList
+            :refresh="true"
+            :get-key="i => i"
+            >
+            <template
+              slot="header"
+              slot-scope="{selected}"
+              >
+              <div class="profile-edit-bulk-actions">
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => muteUsers(selected)"
+                  >
+                  {{ $t('user_card.mute') }}
+                  <template slot="progress">
+                    {{ $t('user_card.mute_progress') }}
+                  </template>
+                </ProgressButton>
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => unmuteUsers(selected)"
+                  >
+                  {{ $t('user_card.unmute') }}
+                  <template slot="progress">
+                    {{ $t('user_card.unmute_progress') }}
+                  </template>
+                </ProgressButton>
+              </div>
+            </template>
+            <template
+              slot="item"
+              slot-scope="{item}"
+              >
+              <MuteCard :user-id="item" />
+            </template>
+            <template slot="empty">
+              {{ $t('settings.no_mutes') }}
+            </template>
+          </MuteList>
+        </div>
+
+        <div :label="$t('settings.domain_mutes')">
+          <div class="profile-edit-domain-mute-form">
+            <input
+              v-model="newDomainToMute"
+              :placeholder="$t('settings.type_domains_to_mute')"
+              type="text"
+              @keyup.enter="muteDomain"
+              >
+            <ProgressButton
+              class="btn btn-default"
+              :click="muteDomain"
+              >
+              {{ $t('domain_mute_card.mute') }}
+              <template slot="progress">
+                {{ $t('domain_mute_card.mute_progress') }}
+              </template>
+            </ProgressButton>
+          </div>
+          <DomainMuteList
+            :refresh="true"
+            :get-key="i => i"
+            >
+            <template
+              slot="header"
+              slot-scope="{selected}"
+              >
+              <div class="profile-edit-bulk-actions">
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => unmuteDomains(selected)"
+                  >
+                  {{ $t('domain_mute_card.unmute') }}
+                  <template slot="progress">
+                    {{ $t('domain_mute_card.unmute_progress') }}
+                  </template>
+                </ProgressButton>
+              </div>
+            </template>
+            <template
+              slot="item"
+              slot-scope="{item}"
+              >
+              <DomainMuteCard :domain="item" />
+            </template>
+            <template slot="empty">
+              {{ $t('settings.no_mutes') }}
+            </template>
+          </DomainMuteList>
+        </div>
+      </tab-switcher>
+    </div>
+  </tab-switcher>
+</template>
+
+<script src="./mutes_and_blocks_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js
deleted file mode 100644
index 0a870b3f..00000000
--- a/src/components/settings_modal/tabs/notifications.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const Notifications = {
-  data () {
-    return {
-      activeTab: 'profile',
-      notificationSettings: this.$store.state.users.currentUser.notification_settings,
-      newDomainToMute: ''
-    }
-  },
-  components: {
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    }
-  },
-  methods: {
-    updateNotificationSettings () {
-      this.$store.state.api.backendInteractor
-        .updateNotificationSettings({ settings: this.notificationSettings })
-    }
-  }
-}
-
-export default Notifications
diff --git a/src/components/settings_modal/tabs/notifications.vue b/src/components/settings_modal/tabs/notifications.vue
deleted file mode 100644
index f9a7c17b..00000000
--- a/src/components/settings_modal/tabs/notifications.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<div :label="$t('settings.notifications')">
-  <div class="setting-item">
-    <div class="select-multiple">
-      <span class="label">{{ $t('settings.notification_setting') }}</span>
-      <ul class="option-list">
-        <li>
-          <Checkbox v-model="notificationSettings.follows">
-            {{ $t('settings.notification_setting_follows') }}
-          </Checkbox>
-        </li>
-        <li>
-          <Checkbox v-model="notificationSettings.followers">
-            {{ $t('settings.notification_setting_followers') }}
-          </Checkbox>
-        </li>
-        <li>
-          <Checkbox v-model="notificationSettings.non_follows">
-            {{ $t('settings.notification_setting_non_follows') }}
-          </Checkbox>
-        </li>
-        <li>
-          <Checkbox v-model="notificationSettings.non_followers">
-            {{ $t('settings.notification_setting_non_followers') }}
-          </Checkbox>
-        </li>
-      </ul>
-    </div>
-    <p>{{ $t('settings.notification_mutes') }}</p>
-    <p>{{ $t('settings.notification_blocks') }}</p>
-    <button
-      class="btn btn-default"
-      @click="updateNotificationSettings"
-      >
-      {{ $t('general.submit') }}
-    </button>
-  </div>
-</div>
-</template>
-
-<script src="./notifications.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
new file mode 100644
index 00000000..3e44c95d
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -0,0 +1,27 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const NotificationsTab = {
+  data () {
+    return {
+      activeTab: 'profile',
+      notificationSettings: this.$store.state.users.currentUser.notification_settings,
+      newDomainToMute: ''
+    }
+  },
+  components: {
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
+  methods: {
+    updateNotificationSettings () {
+      this.$store.state.api.backendInteractor
+        .updateNotificationSettings({ settings: this.notificationSettings })
+    }
+  }
+}
+
+export default NotificationsTab
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
new file mode 100644
index 00000000..ab33a6a5
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -0,0 +1,42 @@
+<template>
+<div :label="$t('settings.notifications')">
+  <div class="setting-item">
+    <div class="select-multiple">
+      <span class="label">{{ $t('settings.notification_setting') }}</span>
+      <ul class="option-list">
+        <li>
+          <Checkbox v-model="notificationSettings.follows">
+            {{ $t('settings.notification_setting_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.followers">
+            {{ $t('settings.notification_setting_followers') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.non_follows">
+            {{ $t('settings.notification_setting_non_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.non_followers">
+            {{ $t('settings.notification_setting_non_followers') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+    <p>{{ $t('settings.notification_mutes') }}</p>
+    <p>{{ $t('settings.notification_blocks') }}</p>
+    <button
+      class="btn btn-default"
+      @click="updateNotificationSettings"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+</div>
+</template>
+
+<script src="./notifications_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js
deleted file mode 100644
index 18c44024..00000000
--- a/src/components/settings_modal/tabs/profile.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import unescape from 'lodash/unescape'
-import ImageCropper from '../../image_cropper/image_cropper.vue'
-import ScopeSelector from '../../scope_selector/scope_selector.vue'
-import fileSizeFormatService from '../../../services/file_size_format/file_size_format.js'
-import ProgressButton from '../../progress_button/progress_button.vue'
-import EmojiInput from '../../emoji_input/emoji_input.vue'
-import suggestor from '../../emoji_input/suggestor.js'
-import Autosuggest from '../../autosuggest/autosuggest.vue'
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const ProfileTab = {
-  data () {
-    return {
-      newName: this.$store.state.users.currentUser.name,
-      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,
-      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,
-      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
-      pickAvatarBtnVisible: true,
-      bannerUploading: false,
-      backgroundUploading: false,
-      banner: null,
-      bannerPreview: null,
-      background: null,
-      backgroundPreview: null,
-      bannerUploadError: null,
-      backgroundUploadError: null,
-    }
-  },
-  components: {
-    ScopeSelector,
-    ImageCropper,
-    EmojiInput,
-    Autosuggest,
-    ProgressButton,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    emojiUserSuggestor () {
-      return suggestor({
-        emoji: [
-          ...this.$store.state.instance.emoji,
-          ...this.$store.state.instance.customEmoji
-        ],
-        users: this.$store.state.users.users,
-        updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
-      })
-    },
-    emojiSuggestor () {
-      return suggestor({ emoji: [
-        ...this.$store.state.instance.emoji,
-        ...this.$store.state.instance.customEmoji
-      ] })
-    }
-  },
-  methods: {
-    updateProfile () {
-      this.$store.state.api.backendInteractor
-        .updateProfile({
-          params: {
-            note: this.newBio,
-            locked: this.newLocked,
-            // Backend notation.
-            /* eslint-disable camelcase */
-            display_name: this.newName,
-            default_scope: this.newDefaultScope,
-            no_rich_text: this.newNoRichText,
-            hide_follows: this.hideFollows,
-            hide_followers: this.hideFollowers,
-            discoverable: this.discoverable,
-            allow_following_move: this.allowFollowingMove,
-            hide_follows_count: this.hideFollowsCount,
-            hide_followers_count: this.hideFollowersCount,
-            show_role: this.showRole
-            /* eslint-enable camelcase */
-          } }).then((user) => {
-          this.$store.commit('addNewUsers', [user])
-          this.$store.commit('setCurrentUser', user)
-        })
-    },
-    changeVis (visibility) {
-      this.newDefaultScope = visibility
-    },
-    uploadFile (slot, e) {
-      const file = e.target.files[0]
-      if (!file) { return }
-      if (file.size > this.$store.state.instance[slot + 'limit']) {
-        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
-        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
-        this[slot + 'UploadError'] = [
-          this.$t('upload.error.base'),
-          this.$t(
-            'upload.error.file_too_big',
-            {
-              filesize: filesize.num,
-              filesizeunit: filesize.unit,
-              allowedsize: allowedsize.num,
-              allowedsizeunit: allowedsize.unit
-            }
-          )
-        ].join(' ')
-        return
-      }
-      // eslint-disable-next-line no-undef
-      const reader = new FileReader()
-      reader.onload = ({ target }) => {
-        const img = target.result
-        this[slot + 'Preview'] = img
-        this[slot] = file
-      }
-      reader.readAsDataURL(file)
-    },
-    submitAvatar (cropper, file) {
-      const that = this
-      return new Promise((resolve, reject) => {
-        function updateAvatar (avatar) {
-          that.$store.state.api.backendInteractor.updateAvatar({ avatar })
-            .then((user) => {
-              that.$store.commit('addNewUsers', [user])
-              that.$store.commit('setCurrentUser', user)
-              resolve()
-            })
-            .catch((err) => {
-              reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
-            })
-        }
-
-        if (cropper) {
-          cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
-        } else {
-          updateAvatar(file)
-        }
-      })
-    },
-    submitBanner () {
-      if (!this.bannerPreview) { return }
-
-      this.bannerUploading = true
-      this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
-        .then((user) => {
-          this.$store.commit('addNewUsers', [user])
-          this.$store.commit('setCurrentUser', user)
-          this.bannerPreview = null
-        })
-        .catch((err) => {
-          this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
-        })
-        .then(() => { this.bannerUploading = false })
-    },
-    submitBg () {
-      if (!this.backgroundPreview) { return }
-      let background = this.background
-      this.backgroundUploading = true
-      this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
-        if (!data.error) {
-          this.$store.commit('addNewUsers', [data])
-          this.$store.commit('setCurrentUser', data)
-          this.backgroundPreview = null
-        } else {
-          this.backgroundUploadError = this.$t('upload.error.base') + data.error
-        }
-        this.backgroundUploading = false
-      })
-    }
-  }
-}
-
-export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile.scss b/src/components/settings_modal/tabs/profile.scss
deleted file mode 100644
index 4aab81eb..00000000
--- a/src/components/settings_modal/tabs/profile.scss
+++ /dev/null
@@ -1,82 +0,0 @@
-@import '../../../_variables.scss';
-.profile-tab {
-  .bio {
-    margin: 0;
-  }
-
-  .visibility-tray {
-    padding-top: 5px;
-  }
-
-  input[type=file] {
-    padding: 5px;
-    height: auto;
-  }
-
-  .banner {
-    max-width: 100%;
-  }
-
-  .uploading {
-    font-size: 1.5em;
-    margin: 0.25em;
-  }
-
-  .name-changer {
-    width: 100%;
-  }
-
-  .bg {
-    max-width: 100%;
-  }
-
-  .current-avatar {
-    display: block;
-    width: 150px;
-    height: 150px;
-    border-radius: $fallback--avatarRadius;
-    border-radius: var(--avatarRadius, $fallback--avatarRadius);
-  }
-
-  .oauth-tokens {
-    width: 100%;
-
-    th {
-      text-align: left;
-    }
-
-    .actions {
-      text-align: right;
-    }
-  }
-
-  &-usersearch-wrapper {
-    padding: 1em;
-  }
-
-  &-bulk-actions {
-    text-align: right;
-    padding: 0 1em;
-    min-height: 28px;
-
-    button {
-      width: 10em;
-    }
-  }
-
-  &-domain-mute-form {
-    padding: 1em;
-    display: flex;
-    flex-direction: column;
-
-    button {
-      align-self: flex-end;
-      margin-top: 1em;
-      width: 10em;
-    }
-  }
-
-  .setting-subitem {
-    margin-left: 1.75em;
-  }
-}
diff --git a/src/components/settings_modal/tabs/profile.vue b/src/components/settings_modal/tabs/profile.vue
deleted file mode 100644
index 335fc12e..00000000
--- a/src/components/settings_modal/tabs/profile.vue
+++ /dev/null
@@ -1,213 +0,0 @@
-<template>
-<div class="profile-tab">
-  <div class="setting-item">
-    <h2>{{ $t('settings.name_bio') }}</h2>
-    <p>{{ $t('settings.name') }}</p>
-    <EmojiInput
-      v-model="newName"
-      enable-emoji-picker
-      :suggest="emojiSuggestor"
-      >
-      <input
-        id="username"
-        v-model="newName"
-        classname="name-changer"
-        >
-    </EmojiInput>
-    <p>{{ $t('settings.bio') }}</p>
-    <EmojiInput
-      v-model="newBio"
-      enable-emoji-picker
-      :suggest="emojiUserSuggestor"
-      >
-      <textarea
-        v-model="newBio"
-        classname="bio"
-        />
-    </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'">
-          {{ $t('settings.show_admin_badge') }}
-        </template>
-        <template v-if="role === 'moderator'">
-          {{ $t('settings.show_moderator_badge') }}
-        </template>
-      </Checkbox>
-    </p>
-    <p>
-      <Checkbox v-model="discoverable">
-        {{ $t('settings.discoverable') }}
-      </Checkbox>
-    </p>
-    <button
-      :disabled="newName && newName.length === 0"
-      class="btn btn-default"
-      @click="updateProfile"
-      >
-      {{ $t('general.submit') }}
-    </button>
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.avatar') }}</h2>
-    <p class="visibility-notice">
-      {{ $t('settings.avatar_size_instruction') }}
-    </p>
-    <p>{{ $t('settings.current_avatar') }}</p>
-    <img
-      :src="user.profile_image_url_original"
-      class="current-avatar"
-      >
-    <p>{{ $t('settings.set_new_avatar') }}</p>
-    <button
-      v-show="pickAvatarBtnVisible"
-      id="pick-avatar"
-      class="btn"
-      type="button"
-      >
-      {{ $t('settings.upload_a_photo') }}
-    </button>
-    <image-cropper
-      trigger="#pick-avatar"
-      :submit-handler="submitAvatar"
-      @open="pickAvatarBtnVisible=false"
-      @close="pickAvatarBtnVisible=true"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.profile_banner') }}</h2>
-    <p>{{ $t('settings.current_profile_banner') }}</p>
-    <img
-      :src="user.cover_photo"
-      class="banner"
-      >
-    <p>{{ $t('settings.set_new_profile_banner') }}</p>
-    <img
-      v-if="bannerPreview"
-      class="banner"
-      :src="bannerPreview"
-      >
-    <div>
-      <input
-        type="file"
-        @change="uploadFile('banner', $event)"
-        >
-    </div>
-    <i
-      v-if="bannerUploading"
-      class=" icon-spin4 animate-spin uploading"
-      />
-    <button
-      v-else-if="bannerPreview"
-      class="btn btn-default"
-      @click="submitBanner"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <div
-      v-if="bannerUploadError"
-      class="alert error"
-      >
-      Error: {{ bannerUploadError }}
-      <i
-        class="button-icon icon-cancel"
-        @click="clearUploadError('banner')"
-        />
-    </div>
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.profile_background') }}</h2>
-    <p>{{ $t('settings.set_new_profile_background') }}</p>
-    <img
-      v-if="backgroundPreview"
-      class="bg"
-      :src="backgroundPreview"
-      >
-    <div>
-      <input
-        type="file"
-        @change="uploadFile('background', $event)"
-        >
-    </div>
-    <i
-      v-if="backgroundUploading"
-      class=" icon-spin4 animate-spin uploading"
-      />
-    <button
-      v-else-if="backgroundPreview"
-      class="btn btn-default"
-      @click="submitBg"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <div
-      v-if="backgroundUploadError"
-      class="alert error"
-      >
-      Error: {{ backgroundUploadError }}
-      <i
-        class="button-icon icon-cancel"
-        @click="clearUploadError('background')"
-        />
-    </div>
-  </div>
-</div>
-</template>
-
-<script src="./profile.js"></script>
-<style lang="scss" src="./profile.scss"></style>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
new file mode 100644
index 00000000..949b480b
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -0,0 +1,179 @@
+import unescape from 'lodash/unescape'
+import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
+import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
+import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+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'
+
+const ProfileTab = {
+  data () {
+    return {
+      newName: this.$store.state.users.currentUser.name,
+      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,
+      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,
+      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
+      pickAvatarBtnVisible: true,
+      bannerUploading: false,
+      backgroundUploading: false,
+      banner: null,
+      bannerPreview: null,
+      background: null,
+      backgroundPreview: null,
+      bannerUploadError: null,
+      backgroundUploadError: null,
+    }
+  },
+  components: {
+    ScopeSelector,
+    ImageCropper,
+    EmojiInput,
+    Autosuggest,
+    ProgressButton,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    emojiUserSuggestor () {
+      return suggestor({
+        emoji: [
+          ...this.$store.state.instance.emoji,
+          ...this.$store.state.instance.customEmoji
+        ],
+        users: this.$store.state.users.users,
+        updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
+      })
+    },
+    emojiSuggestor () {
+      return suggestor({ emoji: [
+        ...this.$store.state.instance.emoji,
+        ...this.$store.state.instance.customEmoji
+      ] })
+    }
+  },
+  methods: {
+    updateProfile () {
+      this.$store.state.api.backendInteractor
+        .updateProfile({
+          params: {
+            note: this.newBio,
+            locked: this.newLocked,
+            // Backend notation.
+            /* eslint-disable camelcase */
+            display_name: this.newName,
+            default_scope: this.newDefaultScope,
+            no_rich_text: this.newNoRichText,
+            hide_follows: this.hideFollows,
+            hide_followers: this.hideFollowers,
+            discoverable: this.discoverable,
+            allow_following_move: this.allowFollowingMove,
+            hide_follows_count: this.hideFollowsCount,
+            hide_followers_count: this.hideFollowersCount,
+            show_role: this.showRole
+            /* eslint-enable camelcase */
+          } }).then((user) => {
+          this.$store.commit('addNewUsers', [user])
+          this.$store.commit('setCurrentUser', user)
+        })
+    },
+    changeVis (visibility) {
+      this.newDefaultScope = visibility
+    },
+    uploadFile (slot, e) {
+      const file = e.target.files[0]
+      if (!file) { return }
+      if (file.size > this.$store.state.instance[slot + 'limit']) {
+        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
+        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
+        this[slot + 'UploadError'] = [
+          this.$t('upload.error.base'),
+          this.$t(
+            'upload.error.file_too_big',
+            {
+              filesize: filesize.num,
+              filesizeunit: filesize.unit,
+              allowedsize: allowedsize.num,
+              allowedsizeunit: allowedsize.unit
+            }
+          )
+        ].join(' ')
+        return
+      }
+      // eslint-disable-next-line no-undef
+      const reader = new FileReader()
+      reader.onload = ({ target }) => {
+        const img = target.result
+        this[slot + 'Preview'] = img
+        this[slot] = file
+      }
+      reader.readAsDataURL(file)
+    },
+    submitAvatar (cropper, file) {
+      const that = this
+      return new Promise((resolve, reject) => {
+        function updateAvatar (avatar) {
+          that.$store.state.api.backendInteractor.updateAvatar({ avatar })
+            .then((user) => {
+              that.$store.commit('addNewUsers', [user])
+              that.$store.commit('setCurrentUser', user)
+              resolve()
+            })
+            .catch((err) => {
+              reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
+            })
+        }
+
+        if (cropper) {
+          cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
+        } else {
+          updateAvatar(file)
+        }
+      })
+    },
+    submitBanner () {
+      if (!this.bannerPreview) { return }
+
+      this.bannerUploading = true
+      this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
+        .then((user) => {
+          this.$store.commit('addNewUsers', [user])
+          this.$store.commit('setCurrentUser', user)
+          this.bannerPreview = null
+        })
+        .catch((err) => {
+          this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
+        })
+        .then(() => { this.bannerUploading = false })
+    },
+    submitBg () {
+      if (!this.backgroundPreview) { return }
+      let background = this.background
+      this.backgroundUploading = true
+      this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
+        if (!data.error) {
+          this.$store.commit('addNewUsers', [data])
+          this.$store.commit('setCurrentUser', data)
+          this.backgroundPreview = null
+        } else {
+          this.backgroundUploadError = this.$t('upload.error.base') + data.error
+        }
+        this.backgroundUploading = false
+      })
+    }
+  }
+}
+
+export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
new file mode 100644
index 00000000..4aab81eb
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -0,0 +1,82 @@
+@import '../../../_variables.scss';
+.profile-tab {
+  .bio {
+    margin: 0;
+  }
+
+  .visibility-tray {
+    padding-top: 5px;
+  }
+
+  input[type=file] {
+    padding: 5px;
+    height: auto;
+  }
+
+  .banner {
+    max-width: 100%;
+  }
+
+  .uploading {
+    font-size: 1.5em;
+    margin: 0.25em;
+  }
+
+  .name-changer {
+    width: 100%;
+  }
+
+  .bg {
+    max-width: 100%;
+  }
+
+  .current-avatar {
+    display: block;
+    width: 150px;
+    height: 150px;
+    border-radius: $fallback--avatarRadius;
+    border-radius: var(--avatarRadius, $fallback--avatarRadius);
+  }
+
+  .oauth-tokens {
+    width: 100%;
+
+    th {
+      text-align: left;
+    }
+
+    .actions {
+      text-align: right;
+    }
+  }
+
+  &-usersearch-wrapper {
+    padding: 1em;
+  }
+
+  &-bulk-actions {
+    text-align: right;
+    padding: 0 1em;
+    min-height: 28px;
+
+    button {
+      width: 10em;
+    }
+  }
+
+  &-domain-mute-form {
+    padding: 1em;
+    display: flex;
+    flex-direction: column;
+
+    button {
+      align-self: flex-end;
+      margin-top: 1em;
+      width: 10em;
+    }
+  }
+
+  .setting-subitem {
+    margin-left: 1.75em;
+  }
+}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
new file mode 100644
index 00000000..9dd89b99
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -0,0 +1,213 @@
+<template>
+<div class="profile-tab">
+  <div class="setting-item">
+    <h2>{{ $t('settings.name_bio') }}</h2>
+    <p>{{ $t('settings.name') }}</p>
+    <EmojiInput
+      v-model="newName"
+      enable-emoji-picker
+      :suggest="emojiSuggestor"
+      >
+      <input
+        id="username"
+        v-model="newName"
+        classname="name-changer"
+        >
+    </EmojiInput>
+    <p>{{ $t('settings.bio') }}</p>
+    <EmojiInput
+      v-model="newBio"
+      enable-emoji-picker
+      :suggest="emojiUserSuggestor"
+      >
+      <textarea
+        v-model="newBio"
+        classname="bio"
+        />
+    </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'">
+          {{ $t('settings.show_admin_badge') }}
+        </template>
+        <template v-if="role === 'moderator'">
+          {{ $t('settings.show_moderator_badge') }}
+        </template>
+      </Checkbox>
+    </p>
+    <p>
+      <Checkbox v-model="discoverable">
+        {{ $t('settings.discoverable') }}
+      </Checkbox>
+    </p>
+    <button
+      :disabled="newName && newName.length === 0"
+      class="btn btn-default"
+      @click="updateProfile"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.avatar') }}</h2>
+    <p class="visibility-notice">
+      {{ $t('settings.avatar_size_instruction') }}
+    </p>
+    <p>{{ $t('settings.current_avatar') }}</p>
+    <img
+      :src="user.profile_image_url_original"
+      class="current-avatar"
+      >
+    <p>{{ $t('settings.set_new_avatar') }}</p>
+    <button
+      v-show="pickAvatarBtnVisible"
+      id="pick-avatar"
+      class="btn"
+      type="button"
+      >
+      {{ $t('settings.upload_a_photo') }}
+    </button>
+    <image-cropper
+      trigger="#pick-avatar"
+      :submit-handler="submitAvatar"
+      @open="pickAvatarBtnVisible=false"
+      @close="pickAvatarBtnVisible=true"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.profile_banner') }}</h2>
+    <p>{{ $t('settings.current_profile_banner') }}</p>
+    <img
+      :src="user.cover_photo"
+      class="banner"
+      >
+    <p>{{ $t('settings.set_new_profile_banner') }}</p>
+    <img
+      v-if="bannerPreview"
+      class="banner"
+      :src="bannerPreview"
+      >
+    <div>
+      <input
+        type="file"
+        @change="uploadFile('banner', $event)"
+        >
+    </div>
+    <i
+      v-if="bannerUploading"
+      class=" icon-spin4 animate-spin uploading"
+      />
+    <button
+      v-else-if="bannerPreview"
+      class="btn btn-default"
+      @click="submitBanner"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <div
+      v-if="bannerUploadError"
+      class="alert error"
+      >
+      Error: {{ bannerUploadError }}
+      <i
+        class="button-icon icon-cancel"
+        @click="clearUploadError('banner')"
+        />
+    </div>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.profile_background') }}</h2>
+    <p>{{ $t('settings.set_new_profile_background') }}</p>
+    <img
+      v-if="backgroundPreview"
+      class="bg"
+      :src="backgroundPreview"
+      >
+    <div>
+      <input
+        type="file"
+        @change="uploadFile('background', $event)"
+        >
+    </div>
+    <i
+      v-if="backgroundUploading"
+      class=" icon-spin4 animate-spin uploading"
+      />
+    <button
+      v-else-if="backgroundPreview"
+      class="btn btn-default"
+      @click="submitBg"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <div
+      v-if="backgroundUploadError"
+      class="alert error"
+      >
+      Error: {{ backgroundUploadError }}
+      <i
+        class="button-icon icon-cancel"
+        @click="clearUploadError('background')"
+        />
+    </div>
+  </div>
+</div>
+</template>
+
+<script src="./profile_tab.js"></script>
+<style lang="scss" src="./profile_tab.scss"></style>
diff --git a/src/components/settings_modal/tabs/security.js b/src/components/settings_modal/tabs/security.js
deleted file mode 100644
index cc791b7a..00000000
--- a/src/components/settings_modal/tabs/security.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import ProgressButton from '../../progress_button/progress_button.vue'
-import Checkbox from '../../checkbox/checkbox.vue'
-import Mfa from '../../user_settings/mfa.vue'
-
-const Security = {
-  data () {
-    return {
-      newEmail: '',
-      changeEmailError: false,
-      changeEmailPassword: '',
-      changedEmail: false,
-      deletingAccount: false,
-      deleteAccountConfirmPasswordInput: '',
-      deleteAccountError: false,
-      changePasswordInputs: [ '', '', '' ],
-      changedPassword: false,
-      changePasswordError: false
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    ProgressButton,
-    Mfa,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    pleromaBackend () {
-      return this.$store.state.instance.pleromaBackend
-    },
-    oauthTokens () {
-      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
-        return {
-          id: oauthToken.id,
-          appName: oauthToken.app_name,
-          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
-        }
-      })
-    }
-  },
-  methods: {
-    confirmDelete () {
-      this.deletingAccount = true
-    },
-    deleteAccount () {
-      this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
-        .then((res) => {
-          if (res.status === 'success') {
-            this.$store.dispatch('logout')
-            this.$router.push({ name: 'root' })
-          } else {
-            this.deleteAccountError = res.error
-          }
-        })
-    },
-    changePassword () {
-      const params = {
-        password: this.changePasswordInputs[0],
-        newPassword: this.changePasswordInputs[1],
-        newPasswordConfirmation: this.changePasswordInputs[2]
-      }
-      this.$store.state.api.backendInteractor.changePassword(params)
-        .then((res) => {
-          if (res.status === 'success') {
-            this.changedPassword = true
-            this.changePasswordError = false
-            this.logout()
-          } else {
-            this.changedPassword = false
-            this.changePasswordError = res.error
-          }
-        })
-    },
-    changeEmail () {
-      const params = {
-        email: this.newEmail,
-        password: this.changeEmailPassword
-      }
-      this.$store.state.api.backendInteractor.changeEmail(params)
-        .then((res) => {
-          if (res.status === 'success') {
-            this.changedEmail = true
-            this.changeEmailError = false
-          } else {
-            this.changedEmail = false
-            this.changeEmailError = res.error
-          }
-        })
-    },
-    logout () {
-      this.$store.dispatch('logout')
-      this.$router.replace('/')
-    },
-    revokeToken (id) {
-      if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
-        this.$store.dispatch('revokeToken', id)
-      }
-    }
-  }
-}
-
-export default Security
diff --git a/src/components/settings_modal/tabs/security.vue b/src/components/settings_modal/tabs/security.vue
deleted file mode 100644
index 603c9a04..00000000
--- a/src/components/settings_modal/tabs/security.vue
+++ /dev/null
@@ -1,143 +0,0 @@
-<template>
-<div :label="$t('settings.security_tab')">
-  <div class="setting-item">
-    <h2>{{ $t('settings.change_email') }}</h2>
-    <div>
-      <p>{{ $t('settings.new_email') }}</p>
-      <input
-        v-model="newEmail"
-        type="email"
-        autocomplete="email"
-        >
-    </div>
-    <div>
-      <p>{{ $t('settings.current_password') }}</p>
-      <input
-        v-model="changeEmailPassword"
-        type="password"
-        autocomplete="current-password"
-        >
-    </div>
-    <button
-      class="btn btn-default"
-      @click="changeEmail"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <p v-if="changedEmail">
-      {{ $t('settings.changed_email') }}
-    </p>
-    <template v-if="changeEmailError !== false">
-      <p>{{ $t('settings.change_email_error') }}</p>
-      <p>{{ changeEmailError }}</p>
-    </template>
-  </div>
-
-  <div class="setting-item">
-    <h2>{{ $t('settings.change_password') }}</h2>
-    <div>
-      <p>{{ $t('settings.current_password') }}</p>
-      <input
-        v-model="changePasswordInputs[0]"
-        type="password"
-        >
-    </div>
-    <div>
-      <p>{{ $t('settings.new_password') }}</p>
-      <input
-        v-model="changePasswordInputs[1]"
-        type="password"
-        >
-    </div>
-    <div>
-      <p>{{ $t('settings.confirm_new_password') }}</p>
-      <input
-        v-model="changePasswordInputs[2]"
-        type="password"
-        >
-    </div>
-    <button
-      class="btn btn-default"
-      @click="changePassword"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <p v-if="changedPassword">
-      {{ $t('settings.changed_password') }}
-    </p>
-    <p v-else-if="changePasswordError !== false">
-      {{ $t('settings.change_password_error') }}
-    </p>
-    <p v-if="changePasswordError">
-      {{ changePasswordError }}
-    </p>
-  </div>
-
-  <div class="setting-item">
-    <h2>{{ $t('settings.oauth_tokens') }}</h2>
-    <table class="oauth-tokens">
-      <thead>
-        <tr>
-          <th>{{ $t('settings.app_name') }}</th>
-          <th>{{ $t('settings.valid_until') }}</th>
-          <th />
-        </tr>
-      </thead>
-      <tbody>
-        <tr
-          v-for="oauthToken in oauthTokens"
-          :key="oauthToken.id"
-          >
-          <td>{{ oauthToken.appName }}</td>
-          <td>{{ oauthToken.validUntil }}</td>
-          <td class="actions">
-            <button
-              class="btn btn-default"
-              @click="revokeToken(oauthToken.id)"
-              >
-              {{ $t('settings.revoke_token') }}
-            </button>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-  <mfa />
-  <div class="setting-item">
-    <h2>{{ $t('settings.delete_account') }}</h2>
-    <p v-if="!deletingAccount">
-      {{ $t('settings.delete_account_description') }}
-    </p>
-    <div v-if="deletingAccount">
-      <p>{{ $t('settings.delete_account_instructions') }}</p>
-      <p>{{ $t('login.password') }}</p>
-      <input
-        v-model="deleteAccountConfirmPasswordInput"
-        type="password"
-        >
-      <button
-        class="btn btn-default"
-        @click="deleteAccount"
-        >
-        {{ $t('settings.delete_account') }}
-      </button>
-    </div>
-    <p v-if="deleteAccountError !== false">
-      {{ $t('settings.delete_account_error') }}
-    </p>
-    <p v-if="deleteAccountError">
-      {{ deleteAccountError }}
-    </p>
-    <button
-      v-if="!deletingAccount"
-      class="btn btn-default"
-      @click="confirmDelete"
-      >
-      {{ $t('general.submit') }}
-    </button>
-  </div>
-</div>
-</template>
-
-<script src="./security.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.js b/src/components/settings_modal/tabs/security_tab/confirm.js
new file mode 100644
index 00000000..0f4ddfc9
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/confirm.js
@@ -0,0 +1,9 @@
+const Confirm = {
+  props: ['disabled'],
+  data: () => ({}),
+  methods: {
+    confirm () { this.$emit('confirm') },
+    cancel () { this.$emit('cancel') }
+  }
+}
+export default Confirm
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue
new file mode 100644
index 00000000..69b3811b
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/confirm.vue
@@ -0,0 +1,22 @@
+<template>
+  <div>
+    <slot />
+    <button
+      class="btn btn-default"
+      :disabled="disabled"
+      @click="confirm"
+    >
+      {{ $t('general.confirm') }}
+    </button>
+    <button
+      class="btn btn-default"
+      :disabled="disabled"
+      @click="cancel"
+    >
+      {{ $t('general.cancel') }}
+    </button>
+  </div>
+</template>
+
+<script src="./confirm.js">
+</script>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js
new file mode 100644
index 00000000..abf37062
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa.js
@@ -0,0 +1,155 @@
+import RecoveryCodes from './mfa_backup_codes.vue'
+import TOTP from './mfa_totp.vue'
+import Confirm from './confirm.vue'
+import VueQrcode from '@chenfengyuan/vue-qrcode'
+import { mapState } from 'vuex'
+
+const Mfa = {
+  data: () => ({
+    settings: { // current settings of MFA
+      available: false,
+      enabled: false,
+      totp: false
+    },
+    setupState: { // setup mfa
+      state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
+      setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
+    },
+    backupCodes: {
+      getNewCodes: false,
+      inProgress: false, //  progress of fetch codes
+      codes: []
+    },
+    otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
+      provisioning_uri: '',
+      key: ''
+    },
+    currentPassword: null,
+    otpConfirmToken: null,
+    error: null,
+    readyInit: false
+  }),
+  components: {
+    'recovery-codes': RecoveryCodes,
+    'totp-item': TOTP,
+    'qrcode': VueQrcode,
+    'confirm': Confirm
+  },
+  computed: {
+    canSetupOTP () {
+      return (
+        (this.setupInProgress && this.backupCodesPrepared) ||
+          this.settings.enabled
+      ) && !this.settings.totp && !this.setupOTPInProgress
+    },
+    setupInProgress () {
+      return this.setupState.state !== '' && this.setupState.state !== 'complete'
+    },
+    setupOTPInProgress () {
+      return this.setupState.state === 'setupOTP' && !this.completedOTP
+    },
+    prepareOTP () {
+      return this.setupState.setupOTPState === 'prepare'
+    },
+    confirmOTP () {
+      return this.setupState.setupOTPState === 'confirm'
+    },
+    completedOTP () {
+      return this.setupState.setupOTPState === 'completed'
+    },
+    backupCodesPrepared () {
+      return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
+    },
+    confirmNewBackupCodes () {
+      return this.backupCodes.getNewCodes
+    },
+    ...mapState({
+      backendInteractor: (state) => state.api.backendInteractor
+    })
+  },
+
+  methods: {
+    activateOTP () {
+      if (!this.settings.enabled) {
+        this.setupState.state = 'getBackupcodes'
+        this.fetchBackupCodes()
+      }
+    },
+    fetchBackupCodes () {
+      this.backupCodes.inProgress = true
+      this.backupCodes.codes = []
+
+      return this.backendInteractor.generateMfaBackupCodes()
+        .then((res) => {
+          this.backupCodes.codes = res.codes
+          this.backupCodes.inProgress = false
+        })
+    },
+    getBackupCodes () { // get a new backup codes
+      this.backupCodes.getNewCodes = true
+    },
+    confirmBackupCodes () { // confirm getting new backup codes
+      this.fetchBackupCodes().then((res) => {
+        this.backupCodes.getNewCodes = false
+      })
+    },
+    cancelBackupCodes () { // cancel confirm form of new backup codes
+      this.backupCodes.getNewCodes = false
+    },
+
+    // Setup OTP
+    setupOTP () { // prepare setup OTP
+      this.setupState.state = 'setupOTP'
+      this.setupState.setupOTPState = 'prepare'
+      this.backendInteractor.mfaSetupOTP()
+        .then((res) => {
+          this.otpSettings = res
+          this.setupState.setupOTPState = 'confirm'
+        })
+    },
+    doConfirmOTP () { // handler confirm enable OTP
+      this.error = null
+      this.backendInteractor.mfaConfirmOTP({
+        token: this.otpConfirmToken,
+        password: this.currentPassword
+      })
+        .then((res) => {
+          if (res.error) {
+            this.error = res.error
+            return
+          }
+          this.completeSetup()
+        })
+    },
+
+    completeSetup () {
+      this.setupState.setupOTPState = 'complete'
+      this.setupState.state = 'complete'
+      this.currentPassword = null
+      this.error = null
+      this.fetchSettings()
+    },
+    cancelSetup () { // cancel setup
+      this.setupState.setupOTPState = ''
+      this.setupState.state = ''
+      this.currentPassword = null
+      this.error = null
+    },
+    // end Setup OTP
+
+    // fetch settings from server
+    async fetchSettings () {
+      let result = await this.backendInteractor.settingsMFA()
+      if (result.error) return
+      this.settings = result.settings
+      this.settings.available = true
+      return result
+    }
+  },
+  mounted () {
+    this.fetchSettings().then(() => {
+      this.readyInit = true
+    })
+  }
+}
+export default Mfa
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue
new file mode 100644
index 00000000..25c4d1dc
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa.vue
@@ -0,0 +1,174 @@
+<template>
+  <div
+    v-if="readyInit && settings.available"
+    class="setting-item mfa-settings"
+  >
+    <div class="mfa-heading">
+      <h2>{{ $t('settings.mfa.title') }}</h2>
+    </div>
+
+    <div>
+      <div
+        v-if="!setupInProgress"
+        class="setting-item"
+      >
+        <!-- Enabled methods -->
+        <h3>{{ $t('settings.mfa.authentication_methods') }}</h3>
+        <totp-item
+          :settings="settings"
+          @deactivate="fetchSettings"
+          @activate="activateOTP"
+        />
+        <br>
+
+        <div v-if="settings.enabled">
+          <!-- backup codes block-->
+          <recovery-codes
+            v-if="!confirmNewBackupCodes"
+            :backup-codes="backupCodes"
+          />
+          <button
+            v-if="!confirmNewBackupCodes"
+            class="btn btn-default"
+            @click="getBackupCodes"
+          >
+            {{ $t('settings.mfa.generate_new_recovery_codes') }}
+          </button>
+
+          <div v-if="confirmNewBackupCodes">
+            <confirm
+              :disabled="backupCodes.inProgress"
+              @confirm="confirmBackupCodes"
+              @cancel="cancelBackupCodes"
+            >
+              <p class="warning">
+                {{ $t('settings.mfa.warning_of_generate_new_codes') }}
+              </p>
+            </confirm>
+          </div>
+        </div>
+      </div>
+
+      <div v-if="setupInProgress">
+        <!-- setup block-->
+
+        <h3>{{ $t('settings.mfa.setup_otp') }}</h3>
+
+        <recovery-codes
+          v-if="!setupOTPInProgress"
+          :backup-codes="backupCodes"
+        />
+
+        <button
+          v-if="canSetupOTP"
+          class="btn btn-default"
+          @click="cancelSetup"
+        >
+          {{ $t('general.cancel') }}
+        </button>
+
+        <button
+          v-if="canSetupOTP"
+          class="btn btn-default"
+          @click="setupOTP"
+        >
+          {{ $t('settings.mfa.setup_otp') }}
+        </button>
+
+        <template v-if="setupOTPInProgress">
+          <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i>
+
+          <div v-if="confirmOTP">
+            <div class="setup-otp">
+              <div class="qr-code">
+                <h4>{{ $t('settings.mfa.scan.title') }}</h4>
+                <p>{{ $t('settings.mfa.scan.desc') }}</p>
+                <qrcode
+                  :value="otpSettings.provisioning_uri"
+                  :options="{ width: 200 }"
+                />
+                <p>
+                  {{ $t('settings.mfa.scan.secret_code') }}:
+                  {{ otpSettings.key }}
+                </p>
+              </div>
+
+              <div class="verify">
+                <h4>{{ $t('general.verify') }}</h4>
+                <p>{{ $t('settings.mfa.verify.desc') }}</p>
+                <input
+                  v-model="otpConfirmToken"
+                  type="text"
+                >
+
+                <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
+                <input
+                  v-model="currentPassword"
+                  type="password"
+                >
+                <div class="confirm-otp-actions">
+                  <button
+                    class="btn btn-default"
+                    @click="doConfirmOTP"
+                  >
+                    {{ $t('settings.mfa.confirm_and_enable') }}
+                  </button>
+                  <button
+                    class="btn btn-default"
+                    @click="cancelSetup"
+                  >
+                    {{ $t('general.cancel') }}
+                  </button>
+                </div>
+                <div
+                  v-if="error"
+                  class="alert error"
+                >
+                  {{ error }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./mfa.js"></script>
+<style lang="scss">
+@import '../../../../_variables.scss';
+.mfa-settings {
+  .mfa-heading, .method-item {
+    overflow: hidden;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    align-items: baseline;
+  }
+
+  .warning {
+    color: $fallback--cOrange;
+    color: var(--cOrange, $fallback--cOrange);
+  }
+
+  .setup-otp {
+    display: flex;
+    justify-content: center;
+    flex-wrap: wrap;
+    .qr-code {
+      flex: 1;
+      padding-right: 10px;
+    }
+    .verify { flex: 1; }
+    .error { margin: 4px 0 0 0; }
+    .confirm-otp-actions {
+      button {
+        width: 15em;
+        margin-top: 5px;
+      }
+
+    }
+  }
+}
+</style>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
new file mode 100644
index 00000000..f0a984ec
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
@@ -0,0 +1,17 @@
+export default {
+  props: {
+    backupCodes: {
+      type: Object,
+      default: () => ({
+        inProgress: false,
+        codes: []
+      })
+    }
+  },
+  data: () => ({}),
+  computed: {
+    inProgress () { return this.backupCodes.inProgress },
+    ready () { return this.backupCodes.codes.length > 0 },
+    displayTitle () { return this.inProgress || this.ready }
+  }
+}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
new file mode 100644
index 00000000..d7e98b3c
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
@@ -0,0 +1,35 @@
+<template>
+  <div class="mfa-backup-codes">
+    <h4 v-if="displayTitle">
+      {{ $t('settings.mfa.recovery_codes') }}
+    </h4>
+    <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i>
+    <template v-if="ready">
+      <p class="alert warning">
+        {{ $t('settings.mfa.recovery_codes_warning') }}
+      </p>
+      <ul class="backup-codes">
+        <li
+          v-for="code in backupCodes.codes"
+          :key="code"
+        >
+          {{ code }}
+        </li>
+      </ul>
+    </template>
+  </div>
+</template>
+<script src="./mfa_backup_codes.js"></script>
+<style lang="scss">
+@import '../../../../_variables.scss';
+
+.mfa-backup-codes {
+  .warning {
+    color: $fallback--cOrange;
+    color: var(--cOrange, $fallback--cOrange);
+  }
+  .backup-codes {
+    font-family: var(--postCodeFont, monospace);
+  }
+}
+</style>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
new file mode 100644
index 00000000..8408d8e9
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
@@ -0,0 +1,49 @@
+import Confirm from './confirm.vue'
+import { mapState } from 'vuex'
+
+export default {
+  props: ['settings'],
+  data: () => ({
+    error: false,
+    currentPassword: '',
+    deactivate: false,
+    inProgress: false // progress peform request to disable otp method
+  }),
+  components: {
+    'confirm': Confirm
+  },
+  computed: {
+    isActivated () {
+      return this.settings.totp
+    },
+    ...mapState({
+      backendInteractor: (state) => state.api.backendInteractor
+    })
+  },
+  methods: {
+    doActivate () {
+      this.$emit('activate')
+    },
+    cancelDeactivate () { this.deactivate = false },
+    doDeactivate () {
+      this.error = null
+      this.deactivate = true
+    },
+    confirmDeactivate () { // confirm deactivate TOTP method
+      this.error = null
+      this.inProgress = true
+      this.backendInteractor.mfaDisableOTP({
+        password: this.currentPassword
+      })
+        .then((res) => {
+          this.inProgress = false
+          if (res.error) {
+            this.error = res.error
+            return
+          }
+          this.deactivate = false
+          this.$emit('deactivate')
+        })
+    }
+  }
+}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
new file mode 100644
index 00000000..c6f2cc7b
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
@@ -0,0 +1,43 @@
+<template>
+  <div>
+    <div class="method-item">
+      <strong>{{ $t('settings.mfa.otp') }}</strong>
+      <button
+        v-if="!isActivated"
+        class="btn btn-default"
+        @click="doActivate"
+      >
+        {{ $t('general.enable') }}
+      </button>
+
+      <button
+        v-if="isActivated"
+        class="btn btn-default"
+        :disabled="deactivate"
+        @click="doDeactivate"
+      >
+        {{ $t('general.disable') }}
+      </button>
+    </div>
+
+    <confirm
+      v-if="deactivate"
+      :disabled="inProgress"
+      @confirm="confirmDeactivate"
+      @cancel="cancelDeactivate"
+    >
+      {{ $t('settings.enter_current_password_to_confirm') }}:
+      <input
+        v-model="currentPassword"
+        type="password"
+      >
+    </confirm>
+    <div
+      v-if="error"
+      class="alert error"
+    >
+      {{ error }}
+    </div>
+  </div>
+</template>
+<script src="./mfa_totp.js"></script>
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js
new file mode 100644
index 00000000..811161a5
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.js
@@ -0,0 +1,106 @@
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Mfa from './mfa.vue'
+
+const SecurityTab = {
+  data () {
+    return {
+      newEmail: '',
+      changeEmailError: false,
+      changeEmailPassword: '',
+      changedEmail: false,
+      deletingAccount: false,
+      deleteAccountConfirmPasswordInput: '',
+      deleteAccountError: false,
+      changePasswordInputs: [ '', '', '' ],
+      changedPassword: false,
+      changePasswordError: false
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    ProgressButton,
+    Mfa,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    pleromaBackend () {
+      return this.$store.state.instance.pleromaBackend
+    },
+    oauthTokens () {
+      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
+        return {
+          id: oauthToken.id,
+          appName: oauthToken.app_name,
+          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+        }
+      })
+    }
+  },
+  methods: {
+    confirmDelete () {
+      this.deletingAccount = true
+    },
+    deleteAccount () {
+      this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
+        .then((res) => {
+          if (res.status === 'success') {
+            this.$store.dispatch('logout')
+            this.$router.push({ name: 'root' })
+          } else {
+            this.deleteAccountError = res.error
+          }
+        })
+    },
+    changePassword () {
+      const params = {
+        password: this.changePasswordInputs[0],
+        newPassword: this.changePasswordInputs[1],
+        newPasswordConfirmation: this.changePasswordInputs[2]
+      }
+      this.$store.state.api.backendInteractor.changePassword(params)
+        .then((res) => {
+          if (res.status === 'success') {
+            this.changedPassword = true
+            this.changePasswordError = false
+            this.logout()
+          } else {
+            this.changedPassword = false
+            this.changePasswordError = res.error
+          }
+        })
+    },
+    changeEmail () {
+      const params = {
+        email: this.newEmail,
+        password: this.changeEmailPassword
+      }
+      this.$store.state.api.backendInteractor.changeEmail(params)
+        .then((res) => {
+          if (res.status === 'success') {
+            this.changedEmail = true
+            this.changeEmailError = false
+          } else {
+            this.changedEmail = false
+            this.changeEmailError = res.error
+          }
+        })
+    },
+    logout () {
+      this.$store.dispatch('logout')
+      this.$router.replace('/')
+    },
+    revokeToken (id) {
+      if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
+        this.$store.dispatch('revokeToken', id)
+      }
+    }
+  }
+}
+
+export default SecurityTab
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
new file mode 100644
index 00000000..45bacec1
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -0,0 +1,143 @@
+<template>
+<div :label="$t('settings.security_tab')">
+  <div class="setting-item">
+    <h2>{{ $t('settings.change_email') }}</h2>
+    <div>
+      <p>{{ $t('settings.new_email') }}</p>
+      <input
+        v-model="newEmail"
+        type="email"
+        autocomplete="email"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.current_password') }}</p>
+      <input
+        v-model="changeEmailPassword"
+        type="password"
+        autocomplete="current-password"
+        >
+    </div>
+    <button
+      class="btn btn-default"
+      @click="changeEmail"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <p v-if="changedEmail">
+      {{ $t('settings.changed_email') }}
+    </p>
+    <template v-if="changeEmailError !== false">
+      <p>{{ $t('settings.change_email_error') }}</p>
+      <p>{{ changeEmailError }}</p>
+    </template>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.change_password') }}</h2>
+    <div>
+      <p>{{ $t('settings.current_password') }}</p>
+      <input
+        v-model="changePasswordInputs[0]"
+        type="password"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.new_password') }}</p>
+      <input
+        v-model="changePasswordInputs[1]"
+        type="password"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.confirm_new_password') }}</p>
+      <input
+        v-model="changePasswordInputs[2]"
+        type="password"
+        >
+    </div>
+    <button
+      class="btn btn-default"
+      @click="changePassword"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <p v-if="changedPassword">
+      {{ $t('settings.changed_password') }}
+    </p>
+    <p v-else-if="changePasswordError !== false">
+      {{ $t('settings.change_password_error') }}
+    </p>
+    <p v-if="changePasswordError">
+      {{ changePasswordError }}
+    </p>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.oauth_tokens') }}</h2>
+    <table class="oauth-tokens">
+      <thead>
+        <tr>
+          <th>{{ $t('settings.app_name') }}</th>
+          <th>{{ $t('settings.valid_until') }}</th>
+          <th />
+        </tr>
+      </thead>
+      <tbody>
+        <tr
+          v-for="oauthToken in oauthTokens"
+          :key="oauthToken.id"
+          >
+          <td>{{ oauthToken.appName }}</td>
+          <td>{{ oauthToken.validUntil }}</td>
+          <td class="actions">
+            <button
+              class="btn btn-default"
+              @click="revokeToken(oauthToken.id)"
+              >
+              {{ $t('settings.revoke_token') }}
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <mfa />
+  <div class="setting-item">
+    <h2>{{ $t('settings.delete_account') }}</h2>
+    <p v-if="!deletingAccount">
+      {{ $t('settings.delete_account_description') }}
+    </p>
+    <div v-if="deletingAccount">
+      <p>{{ $t('settings.delete_account_instructions') }}</p>
+      <p>{{ $t('login.password') }}</p>
+      <input
+        v-model="deleteAccountConfirmPasswordInput"
+        type="password"
+        >
+      <button
+        class="btn btn-default"
+        @click="deleteAccount"
+        >
+        {{ $t('settings.delete_account') }}
+      </button>
+    </div>
+    <p v-if="deleteAccountError !== false">
+      {{ $t('settings.delete_account_error') }}
+    </p>
+    <p v-if="deleteAccountError">
+      {{ deleteAccountError }}
+    </p>
+    <button
+      v-if="!deletingAccount"
+      class="btn btn-default"
+      @click="confirmDelete"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+</div>
+</template>
+
+<script src="./security_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
new file mode 100644
index 00000000..9d984659
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -0,0 +1,117 @@
+<template>
+  <div class="preview-container">
+    <div class="underlay underlay-preview" />
+    <div class="panel dummy">
+      <div class="panel-heading">
+        <div class="title">
+          {{ $t('settings.style.preview.header') }}
+          <span class="badge badge-notification">
+            99
+          </span>
+        </div>
+        <span class="faint">
+          {{ $t('settings.style.preview.header_faint') }}
+        </span>
+        <span class="alert error">
+          {{ $t('settings.style.preview.error') }}
+        </span>
+        <button class="btn">
+          {{ $t('settings.style.preview.button') }}
+        </button>
+      </div>
+      <div class="panel-body theme-preview-content">
+        <div class="post">
+          <div class="avatar still-image">
+            ( ͡° ͜ʖ ͡°)
+          </div>
+          <div class="content">
+            <h4>
+              {{ $t('settings.style.preview.content') }}
+            </h4>
+
+            <i18n path="settings.style.preview.text">
+              <code style="font-family: var(--postCodeFont)">
+                {{ $t('settings.style.preview.mono') }}
+              </code>
+              <a style="color: var(--link)">
+                {{ $t('settings.style.preview.link') }}
+              </a>
+            </i18n>
+
+            <div class="icons">
+              <i
+                style="color: var(--cBlue)"
+                class="button-icon icon-reply"
+              />
+              <i
+                style="color: var(--cGreen)"
+                class="button-icon icon-retweet"
+              />
+              <i
+                style="color: var(--cOrange)"
+                class="button-icon icon-star"
+              />
+              <i
+                style="color: var(--cRed)"
+                class="button-icon icon-cancel"
+              />
+            </div>
+          </div>
+        </div>
+
+        <div class="after-post">
+          <div class="avatar-alt">
+            :^)
+          </div>
+          <div class="content">
+            <i18n
+              path="settings.style.preview.fine_print"
+              tag="span"
+              class="faint"
+            >
+              <a style="color: var(--faintLink)">
+                {{ $t('settings.style.preview.faint_link') }}
+              </a>
+            </i18n>
+          </div>
+        </div>
+        <div class="separator" />
+
+        <span class="alert error">
+          {{ $t('settings.style.preview.error') }}
+        </span>
+        <input
+          :value="$t('settings.style.preview.input')"
+          type="text"
+        >
+
+        <div class="actions">
+          <span class="checkbox">
+            <input
+              id="preview_checkbox"
+              checked="very yes"
+              type="checkbox"
+            >
+            <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
+          </span>
+          <button class="btn">
+            {{ $t('settings.style.preview.button') }}
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss">
+.preview-container {
+  position: relative;
+}
+.underlay-preview {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 10px;
+  right: 10px;
+}
+</style>
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
new file mode 100644
index 00000000..9d61b0c4
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -0,0 +1,759 @@
+import { set, delete as del } from 'vue'
+import {
+  rgb2hex,
+  hex2rgb,
+  getContrastRatioLayers
+} from 'src/services/color_convert/color_convert.js'
+import {
+  DEFAULT_SHADOWS,
+  generateColors,
+  generateShadows,
+  generateRadii,
+  generateFonts,
+  composePreset,
+  getThemes,
+  shadows2to3,
+  colors2to3
+} from 'src/services/style_setter/style_setter.js'
+import {
+  SLOT_INHERITANCE
+} from 'src/services/theme_data/pleromafe.js'
+import {
+  CURRENT_VERSION,
+  OPACITIES,
+  getLayers,
+  getOpacitySlot
+} from 'src/services/theme_data/theme_data.service.js'
+import ColorInput from 'src/components/color_input/color_input.vue'
+import RangeInput from 'src/components/range_input/range_input.vue'
+import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
+import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
+import FontControl from 'src/components/font_control/font_control.vue'
+import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import ExportImport from 'src/components/export_import/export_import.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import Preview from './preview.vue'
+
+// List of color values used in v1
+const v1OnlyNames = [
+  'bg',
+  'fg',
+  'text',
+  'link',
+  'cRed',
+  'cGreen',
+  'cBlue',
+  'cOrange'
+].map(_ => _ + 'ColorLocal')
+
+const colorConvert = (color) => {
+  if (color.startsWith('--') || color === 'transparent') {
+    return color
+  } else {
+    return hex2rgb(color)
+  }
+}
+
+export default {
+  data () {
+    return {
+      availableStyles: [],
+      selected: this.$store.getters.mergedConfig.theme,
+      themeWarning: undefined,
+      tempImportFile: undefined,
+      engineVersion: 0,
+
+      previewShadows: {},
+      previewColors: {},
+      previewRadii: {},
+      previewFonts: {},
+
+      shadowsInvalid: true,
+      colorsInvalid: true,
+      radiiInvalid: true,
+
+      keepColor: false,
+      keepShadows: false,
+      keepOpacity: false,
+      keepRoundness: false,
+      keepFonts: false,
+
+      ...Object.keys(SLOT_INHERITANCE)
+        .map(key => [key, ''])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
+
+      ...Object.keys(OPACITIES)
+        .map(key => [key, ''])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
+
+      shadowSelected: undefined,
+      shadowsLocal: {},
+      fontsLocal: {},
+
+      btnRadiusLocal: '',
+      inputRadiusLocal: '',
+      checkboxRadiusLocal: '',
+      panelRadiusLocal: '',
+      avatarRadiusLocal: '',
+      avatarAltRadiusLocal: '',
+      attachmentRadiusLocal: '',
+      tooltipRadiusLocal: ''
+    }
+  },
+  created () {
+    const self = this
+
+    getThemes()
+      .then((promises) => {
+        return Promise.all(
+          Object.entries(promises)
+            .map(([k, v]) => v.then(res => [k, res]))
+        )
+      })
+      .then(themes => themes.reduce((acc, [k, v]) => {
+        if (v) {
+          return {
+            ...acc,
+            [k]: v
+          }
+        } else {
+          return acc
+        }
+      }, {}))
+      .then((themesComplete) => {
+        self.availableStyles = themesComplete
+      })
+  },
+  mounted () {
+    this.loadThemeFromLocalStorage()
+    if (typeof this.shadowSelected === 'undefined') {
+      this.shadowSelected = this.shadowsAvailable[0]
+    }
+  },
+  computed: {
+    themeWarningHelp () {
+      if (!this.themeWarning) return
+      const t = this.$t
+      const pre = 'settings.style.switcher.help.'
+      const {
+        origin,
+        themeEngineVersion,
+        type,
+        noActionsPossible
+      } = this.themeWarning
+      if (origin === 'file') {
+        // Loaded v2 theme from file
+        if (themeEngineVersion === 2 && type === 'wrong_version') {
+          return t(pre + 'v2_imported')
+        }
+        if (themeEngineVersion > CURRENT_VERSION) {
+          return t(pre + 'future_version_imported') + ' ' +
+            (
+              noActionsPossible
+                ? t(pre + 'snapshot_missing')
+                : t(pre + 'snapshot_present')
+            )
+        }
+        if (themeEngineVersion < CURRENT_VERSION) {
+          return t(pre + 'future_version_imported') + ' ' +
+            (
+              noActionsPossible
+                ? t(pre + 'snapshot_missing')
+                : t(pre + 'snapshot_present')
+            )
+        }
+      } else if (origin === 'localStorage') {
+        if (type === 'snapshot_source_mismatch') {
+          return t(pre + 'snapshot_source_mismatch')
+        }
+        // FE upgraded from v2
+        if (themeEngineVersion === 2) {
+          return t(pre + 'upgraded_from_v2')
+        }
+        // Admin downgraded FE
+        if (themeEngineVersion > CURRENT_VERSION) {
+          return t(pre + 'fe_downgraded') + ' ' +
+            (
+              noActionsPossible
+                ? t(pre + 'migration_snapshot_ok')
+                : t(pre + 'migration_snapshot_gone')
+            )
+        }
+        // Admin upgraded FE
+        if (themeEngineVersion < CURRENT_VERSION) {
+          return t(pre + 'fe_upgraded') + ' ' +
+            (
+              noActionsPossible
+                ? t(pre + 'migration_snapshot_ok')
+                : t(pre + 'migration_snapshot_gone')
+            )
+        }
+      }
+    },
+    selectedVersion () {
+      return Array.isArray(this.selected) ? 1 : 2
+    },
+    currentColors () {
+      return Object.keys(SLOT_INHERITANCE)
+        .map(key => [key, this[key + 'ColorLocal']])
+        .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 }), {})
+    },
+    currentRadii () {
+      return {
+        btn: this.btnRadiusLocal,
+        input: this.inputRadiusLocal,
+        checkbox: this.checkboxRadiusLocal,
+        panel: this.panelRadiusLocal,
+        avatar: this.avatarRadiusLocal,
+        avatarAlt: this.avatarAltRadiusLocal,
+        tooltip: this.tooltipRadiusLocal,
+        attachment: this.attachmentRadiusLocal
+      }
+    },
+    preview () {
+      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
+    },
+    previewTheme () {
+      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
+      return this.preview.theme
+    },
+    // This needs optimization maybe
+    previewContrast () {
+      try {
+        if (!this.previewTheme.colors.bg) return {}
+        const colors = this.previewTheme.colors
+        const opacity = this.previewTheme.opacity
+        if (!colors.bg) return {}
+        const hints = (ratio) => ({
+          text: ratio.toPrecision(3) + ':1',
+          // AA level, AAA level
+          aa: ratio >= 4.5,
+          aaa: ratio >= 7,
+          // same but for 18pt+ texts
+          laa: ratio >= 3,
+          laaa: ratio >= 4.5
+        })
+        const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
+
+        const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
+          const slotIsBaseText = key === 'text' || key === 'link'
+          const slotIsText = slotIsBaseText || (
+            typeof value === 'object' && value !== null && value.textColor
+          )
+          if (!slotIsText) return acc
+          const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
+          const background = variant || layer
+          const opacitySlot = getOpacitySlot(background)
+          const textColors = [
+            key,
+            ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
+          ]
+
+          const layers = getLayers(
+            layer,
+            variant || layer,
+            opacitySlot,
+            colorsConverted,
+            opacity
+          )
+
+          return {
+            ...acc,
+            ...textColors.reduce((acc, textColorKey) => {
+              const newKey = slotIsBaseText
+                ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
+                : textColorKey
+              return {
+                ...acc,
+                [newKey]: getContrastRatioLayers(
+                  colorsConverted[textColorKey],
+                  layers,
+                  colorsConverted[textColorKey]
+                )
+              }
+            }, {})
+          }
+        }, {})
+
+        return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
+      } catch (e) {
+        console.warn('Failure computing contrasts', e)
+      }
+    },
+    previewRules () {
+      if (!this.preview.rules) return ''
+      return [
+        ...Object.values(this.preview.rules),
+        'color: var(--text)',
+        'font-family: var(--interfaceFont, sans-serif)'
+      ].join(';')
+    },
+    shadowsAvailable () {
+      return Object.keys(DEFAULT_SHADOWS).sort()
+    },
+    currentShadowOverriden: {
+      get () {
+        return !!this.currentShadow
+      },
+      set (val) {
+        if (val) {
+          set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
+        } else {
+          del(this.shadowsLocal, this.shadowSelected)
+        }
+      }
+    },
+    currentShadowFallback () {
+      return (this.previewTheme.shadows || {})[this.shadowSelected]
+    },
+    currentShadow: {
+      get () {
+        return this.shadowsLocal[this.shadowSelected]
+      },
+      set (v) {
+        set(this.shadowsLocal, this.shadowSelected, v)
+      }
+    },
+    themeValid () {
+      return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
+    },
+    exportedTheme () {
+      const saveEverything = (
+        !this.keepFonts &&
+        !this.keepShadows &&
+        !this.keepOpacity &&
+        !this.keepRoundness &&
+        !this.keepColor
+      )
+
+      const source = {
+        themeEngineVersion: CURRENT_VERSION
+      }
+
+      if (this.keepFonts || saveEverything) {
+        source.fonts = this.fontsLocal
+      }
+      if (this.keepShadows || saveEverything) {
+        source.shadows = this.shadowsLocal
+      }
+      if (this.keepOpacity || saveEverything) {
+        source.opacity = this.currentOpacity
+      }
+      if (this.keepColor || saveEverything) {
+        source.colors = this.currentColors
+      }
+      if (this.keepRoundness || saveEverything) {
+        source.radii = this.currentRadii
+      }
+
+      const theme = {
+        themeEngineVersion: CURRENT_VERSION,
+        ...this.previewTheme
+      }
+
+      return {
+        // To separate from other random JSON files and possible future source formats
+        _pleroma_theme_version: 2, theme, source
+      }
+    }
+  },
+  components: {
+    ColorInput,
+    OpacityInput,
+    RangeInput,
+    ContrastRatio,
+    ShadowControl,
+    FontControl,
+    TabSwitcher,
+    Preview,
+    ExportImport,
+    Checkbox
+  },
+  methods: {
+    loadTheme (
+      {
+        theme,
+        source,
+        _pleroma_theme_version: fileVersion
+      },
+      origin,
+      forceUseSource = false
+    ) {
+      this.dismissWarning()
+      if (!source && !theme) {
+        throw new Error('Can\'t load theme: empty')
+      }
+      const version = (origin === 'localStorage' && !theme.colors)
+        ? 'l1'
+        : fileVersion
+      const snapshotEngineVersion = (theme || {}).themeEngineVersion
+      const themeEngineVersion = (source || {}).themeEngineVersion || 2
+      const versionsMatch = themeEngineVersion === CURRENT_VERSION
+      const sourceSnapshotMismatch = (
+        theme !== undefined &&
+          source !== undefined &&
+          themeEngineVersion !== snapshotEngineVersion
+      )
+      // Force loading of source if user requested it or if snapshot
+      // is unavailable
+      const forcedSourceLoad = (source && forceUseSource) || !theme
+      if (!(versionsMatch && !sourceSnapshotMismatch) &&
+          !forcedSourceLoad &&
+          version !== 'l1' &&
+          origin !== 'defaults'
+      ) {
+        if (sourceSnapshotMismatch && origin === 'localStorage') {
+          this.themeWarning = {
+            origin,
+            themeEngineVersion,
+            type: 'snapshot_source_mismatch'
+          }
+        } else if (!theme) {
+          this.themeWarning = {
+            origin,
+            noActionsPossible: true,
+            themeEngineVersion,
+            type: 'no_snapshot_old_version'
+          }
+        } else if (!versionsMatch) {
+          this.themeWarning = {
+            origin,
+            noActionsPossible: !source,
+            themeEngineVersion,
+            type: 'wrong_version'
+          }
+        }
+      }
+      this.normalizeLocalState(theme, version, source, forcedSourceLoad)
+    },
+    forceLoadLocalStorage () {
+      this.loadThemeFromLocalStorage(true)
+    },
+    dismissWarning () {
+      this.themeWarning = undefined
+      this.tempImportFile = undefined
+    },
+    forceLoad () {
+      const { origin } = this.themeWarning
+      switch (origin) {
+        case 'localStorage':
+          this.loadThemeFromLocalStorage(true)
+          break
+        case 'file':
+          this.onImport(this.tempImportFile, true)
+          break
+      }
+      this.dismissWarning()
+    },
+    forceSnapshot () {
+      const { origin } = this.themeWarning
+      switch (origin) {
+        case 'localStorage':
+          this.loadThemeFromLocalStorage(false, true)
+          break
+        case 'file':
+          console.err('Forcing snapshout from file is not supported yet')
+          break
+      }
+      this.dismissWarning()
+    },
+    loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
+      const {
+        customTheme: theme,
+        customThemeSource: source
+      } = this.$store.getters.mergedConfig
+      if (!theme && !source) {
+        // Anon user or never touched themes
+        this.loadTheme(
+          this.$store.state.instance.themeData,
+          'defaults',
+          confirmLoadSource
+        )
+      } else {
+        this.loadTheme(
+          {
+            theme,
+            source: forceSnapshot ? theme : source
+          },
+          'localStorage',
+          confirmLoadSource
+        )
+      }
+    },
+    setCustomTheme () {
+      this.$store.dispatch('setOption', {
+        name: 'customTheme',
+        value: {
+          themeEngineVersion: CURRENT_VERSION,
+          ...this.previewTheme
+        }
+      })
+      this.$store.dispatch('setOption', {
+        name: 'customThemeSource',
+        value: {
+          themeEngineVersion: CURRENT_VERSION,
+          shadows: this.shadowsLocal,
+          fonts: this.fontsLocal,
+          opacity: this.currentOpacity,
+          colors: this.currentColors,
+          radii: this.currentRadii
+        }
+      })
+    },
+    updatePreviewColorsAndShadows () {
+      this.previewColors = generateColors({
+        opacity: this.currentOpacity,
+        colors: this.currentColors
+      })
+      this.previewShadows = generateShadows(
+        { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
+        this.previewColors.theme.colors,
+        this.previewColors.mod
+      )
+    },
+    onImport (parsed, forceSource = false) {
+      this.tempImportFile = parsed
+      this.loadTheme(parsed, 'file', forceSource)
+    },
+    importValidator (parsed) {
+      const version = parsed._pleroma_theme_version
+      return version >= 1 || version <= 2
+    },
+    clearAll () {
+      this.loadThemeFromLocalStorage()
+    },
+
+    // Clears all the extra stuff when loading V1 theme
+    clearV1 () {
+      Object.keys(this.$data)
+        .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
+        .filter(_ => !v1OnlyNames.includes(_))
+        .forEach(key => {
+          set(this.$data, key, undefined)
+        })
+    },
+
+    clearRoundness () {
+      Object.keys(this.$data)
+        .filter(_ => _.endsWith('RadiusLocal'))
+        .forEach(key => {
+          set(this.$data, key, undefined)
+        })
+    },
+
+    clearOpacity () {
+      Object.keys(this.$data)
+        .filter(_ => _.endsWith('OpacityLocal'))
+        .forEach(key => {
+          set(this.$data, key, undefined)
+        })
+    },
+
+    clearShadows () {
+      this.shadowsLocal = {}
+    },
+
+    clearFonts () {
+      this.fontsLocal = {}
+    },
+
+    /**
+     * This applies stored theme data onto form. Supports three versions of data:
+     * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
+     * v2 (version = 2) - newer version of themes.
+     * v1 (version = 1) - older version of themes (import from file)
+     * v1l (version = l1) - older version of theme (load from local storage)
+     * v1 and v1l differ because of way themes were stored/exported.
+     * @param {Object} theme - theme data (snapshot)
+     * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
+     * @param {Object} source - theme source - this will be used if compatible
+     * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
+     *                           this allows importing source anyway
+     */
+    normalizeLocalState (theme, version = 0, source, forceSource = false) {
+      let input
+      if (typeof source !== 'undefined') {
+        if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
+          input = source
+          version = source.themeEngineVersion
+        } else {
+          input = theme
+        }
+      } else {
+        input = theme
+      }
+
+      const radii = input.radii || input
+      const opacity = input.opacity
+      const shadows = input.shadows || {}
+      const fonts = input.fonts || {}
+      const colors = !input.themeEngineVersion
+        ? colors2to3(input.colors || input)
+        : input.colors || input
+
+      if (version === 0) {
+        if (input.version) version = input.version
+        // Old v1 naming: fg is text, btn is foreground
+        if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
+          version = 1
+        }
+        // New v2 naming: text is text, fg is foreground
+        if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
+          version = 2
+        }
+      }
+
+      this.engineVersion = version
+
+      // Stuff that differs between V1 and V2
+      if (version === 1) {
+        this.fgColorLocal = rgb2hex(colors.btn)
+        this.textColorLocal = rgb2hex(colors.fg)
+      }
+
+      if (!this.keepColor) {
+        this.clearV1()
+        const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
+        if (version === 1 || version === 'l1') {
+          keys
+            .add('bg')
+            .add('link')
+            .add('cRed')
+            .add('cBlue')
+            .add('cGreen')
+            .add('cOrange')
+        }
+
+        keys.forEach(key => {
+          const color = colors[key]
+          const hex = rgb2hex(colors[key])
+          this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
+        })
+      }
+
+      if (opacity && !this.keepOpacity) {
+        this.clearOpacity()
+        Object.entries(opacity).forEach(([k, v]) => {
+          if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
+          this[k + 'OpacityLocal'] = v
+        })
+      }
+
+      if (!this.keepRoundness) {
+        this.clearRoundness()
+        Object.entries(radii).forEach(([k, v]) => {
+          // 'Radius' is kept mostly for v1->v2 localstorage transition
+          const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
+          this[key + 'RadiusLocal'] = v
+        })
+      }
+
+      if (!this.keepShadows) {
+        this.clearShadows()
+        if (version === 2) {
+          this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
+        } else {
+          this.shadowsLocal = shadows
+        }
+        this.shadowSelected = this.shadowsAvailable[0]
+      }
+
+      if (!this.keepFonts) {
+        this.clearFonts()
+        this.fontsLocal = fonts
+      }
+    }
+  },
+  watch: {
+    currentRadii () {
+      try {
+        this.previewRadii = generateRadii({ radii: this.currentRadii })
+        this.radiiInvalid = false
+      } catch (e) {
+        this.radiiInvalid = true
+        console.warn(e)
+      }
+    },
+    shadowsLocal: {
+      handler () {
+        if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
+        try {
+          this.updatePreviewColorsAndShadows()
+          this.shadowsInvalid = false
+        } catch (e) {
+          this.shadowsInvalid = true
+          console.warn(e)
+        }
+      },
+      deep: true
+    },
+    fontsLocal: {
+      handler () {
+        try {
+          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
+          this.fontsInvalid = false
+        } catch (e) {
+          this.fontsInvalid = true
+          console.warn(e)
+        }
+      },
+      deep: true
+    },
+    currentColors () {
+      try {
+        this.updatePreviewColorsAndShadows()
+        this.colorsInvalid = false
+        this.shadowsInvalid = false
+      } catch (e) {
+        this.colorsInvalid = true
+        this.shadowsInvalid = true
+        console.warn(e)
+      }
+    },
+    currentOpacity () {
+      try {
+        this.updatePreviewColorsAndShadows()
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+    selected () {
+      this.dismissWarning()
+      if (this.selectedVersion === 1) {
+        if (!this.keepRoundness) {
+          this.clearRoundness()
+        }
+
+        if (!this.keepShadows) {
+          this.clearShadows()
+        }
+
+        if (!this.keepOpacity) {
+          this.clearOpacity()
+        }
+
+        if (!this.keepColor) {
+          this.clearV1()
+
+          this.bgColorLocal = this.selected[1]
+          this.fgColorLocal = this.selected[2]
+          this.textColorLocal = this.selected[3]
+          this.linkColorLocal = this.selected[4]
+          this.cRedColorLocal = this.selected[5]
+          this.cGreenColorLocal = this.selected[6]
+          this.cBlueColorLocal = this.selected[7]
+          this.cOrangeColorLocal = this.selected[8]
+        }
+      } else if (this.selectedVersion >= 2) {
+        this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
+      }
+    }
+  }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
new file mode 100644
index 00000000..75b3017d
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -0,0 +1,339 @@
+@import 'src/_variables.scss';
+.theme-tab {
+  .theme-warning {
+    display: flex;
+    align-items: baseline;
+    margin-bottom: .5em;
+    .buttons {
+      .btn {
+        margin-bottom: .5em;
+      }
+    }
+  }
+  .preset-switcher {
+    margin-right: 1em;
+  }
+
+  .style-control {
+    display: flex;
+    align-items: baseline;
+    margin-bottom: 5px;
+
+    .label {
+      flex: 1;
+    }
+
+    &.disabled {
+      input, select {
+        opacity: .5
+      }
+    }
+
+    .opt {
+      margin: .5em;
+    }
+
+    .color-input {
+      flex: 0 0 0;
+    }
+
+    input, select {
+      min-width: 3em;
+      margin: 0;
+      flex: 0;
+
+      &[type=number] {
+        min-width: 5em;
+      }
+
+      &[type=range] {
+        flex: 1;
+        min-width: 3em;
+        align-self: flex-start;
+      }
+    }
+  }
+
+  .reset-container {
+    flex-wrap: wrap;
+  }
+
+  .fonts-container,
+  .reset-container,
+  .apply-container,
+  .radius-container,
+  .color-container,
+  {
+    display: flex;
+  }
+
+  .fonts-container,
+  .radius-container {
+    flex-direction: column;
+  }
+
+  .color-container{
+    > h4 {
+      width: 99%;
+    }
+    flex-wrap: wrap;
+    justify-content: space-between;
+  }
+
+  .fonts-container,
+  .color-container,
+  .shadow-container,
+  .radius-container,
+  .presets-container {
+    margin: 1em 1em 0;
+  }
+
+  .tab-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: baseline;
+    width: 100%;
+    min-height: 30px;
+
+    .btn {
+      min-width: 1px;
+      flex: 0 auto;
+      padding: 0 1em;
+    }
+
+    p {
+      flex: 1;
+      margin: 0;
+      margin-right: .5em;
+    }
+
+    margin-bottom: 1em;
+  }
+
+  .shadow-selector {
+    .override {
+      flex: 1;
+      margin-left: .5em;
+    }
+    .select-container {
+      margin-top: -4px;
+      margin-bottom: -3px;
+    }
+  }
+
+  .save-load,
+  .save-load-options {
+    display: flex;
+    justify-content: center;
+    align-items: baseline;
+    flex-wrap: wrap;
+
+    .presets,
+    .import-export {
+      margin-bottom: .5em;
+    }
+
+    .import-export {
+      display: flex;
+    }
+
+    .override {
+      margin-left: .5em;
+    }
+  }
+
+  .save-load-options {
+    flex-wrap: wrap;
+    margin-top: .5em;
+    justify-content: center;
+    .keep-option {
+      margin: 0 .5em .5em;
+      min-width: 25%;
+    }
+  }
+
+  .preview-container {
+    border-top: 1px dashed;
+    border-bottom: 1px dashed;
+    border-color: $fallback--border;
+    border-color: var(--border, $fallback--border);
+    margin: 1em 0;
+    padding: 1em;
+    background: var(--body-background-image);
+    background-size: cover;
+    background-position: 50% 50%;
+
+    .dummy {
+      .post {
+        font-family: var(--postFont);
+        display: flex;
+
+        .content {
+          flex: 1;
+
+          h4 {
+            margin-bottom: .25em;
+          }
+
+          .icons {
+            margin-top: .5em;
+            display: flex;
+
+            i {
+              margin-right: 1em;
+            }
+          }
+        }
+      }
+
+      .after-post {
+        margin-top: 1em;
+        display: flex;
+        align-items: center;
+      }
+
+      .avatar, .avatar-alt{
+        background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
+        color: black;
+        font-family: sans-serif;
+        text-align: center;
+        margin-right: 1em;
+      }
+
+      .avatar-alt {
+        flex: 0 auto;
+        margin-left: 28px;
+        font-size: 12px;
+        min-width: 20px;
+        min-height: 20px;
+        line-height: 20px;
+        border-radius: $fallback--avatarAltRadius;
+        border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+      }
+
+      .avatar {
+        flex: 0 auto;
+        width: 48px;
+        height: 48px;
+        font-size: 14px;
+        line-height: 48px;
+      }
+
+      .actions {
+        display: flex;
+        align-items: baseline;
+
+        .checkbox {
+          display: inline-flex;
+          align-items: baseline;
+          margin-right: 1em;
+          flex: 1;
+        }
+      }
+
+      .separator {
+        margin: 1em;
+        border-bottom: 1px solid;
+        border-color: $fallback--border;
+        border-color: var(--border, $fallback--border);
+      }
+
+      .panel-heading {
+        .badge, .alert, .btn, .faint {
+          margin-left: 1em;
+          white-space: nowrap;
+        }
+        .faint {
+          text-overflow: ellipsis;
+          min-width: 2em;
+          overflow-x: hidden;
+        }
+        .flex-spacer {
+          flex: 1;
+        }
+      }
+      .btn {
+        margin-left: 0;
+        padding: 0 1em;
+        min-width: 3em;
+        min-height: 30px;
+      }
+    }
+  }
+
+  .apply-container {
+    justify-content: center;
+  }
+
+  .radius-item,
+  .color-item {
+    min-width: 20em;
+    margin: 5px 6px 0 0;
+    display:flex;
+    flex-direction: column;
+    flex: 1 1 0;
+
+    &.wide {
+      min-width: 60%
+    }
+
+    &:not(.wide):nth-child(2n+1) {
+      margin-right: 7px;
+
+    }
+
+    .color, .opacity {
+      display:flex;
+      align-items: baseline;
+    }
+  }
+
+  .radius-item {
+    flex-basis: auto;
+  }
+
+  .theme-radius-rn,
+  .theme-color-cl {
+    border: 0;
+    box-shadow: none;
+    background: transparent;
+    color: var(--faint, $fallback--faint);
+    align-self: stretch;
+  }
+
+  .theme-color-cl,
+  .theme-radius-in,
+  .theme-color-in {
+    margin-left: 4px;
+  }
+
+  .theme-radius-in {
+    min-width: 1em;
+  }
+
+  .theme-radius-in {
+    max-width: 7em;
+    flex: 1;
+  }
+
+  .theme-radius-lb{
+    max-width: 50em;
+  }
+
+  .theme-preview-content {
+    padding: 20px;
+  }
+
+  .apply-container {
+    .btn {
+      min-height: 28px;
+      min-width: 10em;
+      padding: 0 2em;
+    }
+  }
+
+  .btn {
+    margin-left: .25em;
+    margin-right: .25em;
+  }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
new file mode 100644
index 00000000..6f6cf1d6
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -0,0 +1,956 @@
+<template>
+<div class="theme-tab">
+  <div class="presets-container">
+    <div class="save-load">
+      <div
+        v-if="themeWarning"
+        class="theme-warning"
+        >
+        <div class="alert warning">
+          {{ themeWarningHelp }}
+        </div>
+        <div class="buttons">
+          <template v-if="themeWarning.type === 'snapshot_source_mismatch'">
+            <button
+              class="btn"
+              @click="forceLoad"
+              >
+              {{ $t('settings.style.switcher.use_source') }}
+            </button>
+            <button
+              class="btn"
+              @click="forceSnapshot"
+              >
+              {{ $t('settings.style.switcher.use_snapshot') }}
+            </button>
+          </template>
+          <template v-else-if="themeWarning.noActionsPossible">
+            <button
+              class="btn"
+              @click="dismissWarning"
+              >
+              {{ $t('general.dismiss') }}
+            </button>
+          </template>
+          <template v-else>
+            <button
+              class="btn"
+              @click="forceLoad"
+              >
+              {{ $t('settings.style.switcher.load_theme') }}
+            </button>
+            <button
+              class="btn"
+              @click="dismissWarning"
+              >
+              {{ $t('settings.style.switcher.keep_as_is') }}
+            </button>
+          </template>
+        </div>
+      </div>
+      <ExportImport
+        :export-object="exportedTheme"
+        :export-label="$t(&quot;settings.export_theme&quot;)"
+        :import-label="$t(&quot;settings.import_theme&quot;)"
+        :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
+        :on-import="onImport"
+        :validator="importValidator"
+        >
+        <template slot="before">
+          <div class="presets">
+            {{ $t('settings.presets') }}
+            <label
+              for="preset-switcher"
+              class="select"
+              >
+              <select
+                id="preset-switcher"
+                v-model="selected"
+                class="preset-switcher"
+                >
+                <option
+                  v-for="style in availableStyles"
+                  :key="style.name"
+                  :value="style"
+                  :style="{
+                          backgroundColor: style[1] || (style.theme || style.source).colors.bg,
+                          color: style[3] || (style.theme || style.source).colors.text
+                          }"
+                  >
+                  {{ style[0] || style.name }}
+                </option>
+              </select>
+              <i class="icon-down-open" />
+            </label>
+          </div>
+        </template>
+      </ExportImport>
+      </div>
+      <div class="save-load-options">
+        <span class="keep-option">
+          <Checkbox v-model="keepColor">
+            {{ $t('settings.style.switcher.keep_color') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepShadows">
+            {{ $t('settings.style.switcher.keep_shadows') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepOpacity">
+            {{ $t('settings.style.switcher.keep_opacity') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepRoundness">
+            {{ $t('settings.style.switcher.keep_roundness') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepFonts">
+            {{ $t('settings.style.switcher.keep_fonts') }}
+          </Checkbox>
+        </span>
+        <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
+      </div>
+    </div>
+
+    <preview :style="previewRules" />
+
+    <keep-alive>
+      <tab-switcher key="style-tweak">
+        <div
+          :label="$t('settings.style.common_colors._tab_label')"
+          class="color-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.theme_help') }}</p>
+            <button
+              class="btn"
+              @click="clearOpacity"
+            >
+              {{ $t('settings.style.switcher.clear_opacity') }}
+            </button>
+            <button
+              class="btn"
+              @click="clearV1"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <p>{{ $t('settings.theme_help_v2_1') }}</p>
+          <h4>{{ $t('settings.style.common_colors.main') }}</h4>
+          <div class="color-item">
+            <ColorInput
+              v-model="bgColorLocal"
+              name="bgColor"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="bgOpacityLocal"
+              name="bgOpacity"
+              :fallback="previewTheme.opacity.bg"
+            />
+            <ColorInput
+              v-model="textColorLocal"
+              name="textColor"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgText" />
+            <ColorInput
+              v-model="accentColorLocal"
+              name="accentColor"
+              :fallback="previewTheme.colors.link"
+              :label="$t('settings.accent')"
+              :show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
+            />
+            <ColorInput
+              v-model="linkColorLocal"
+              name="linkColor"
+              :fallback="previewTheme.colors.accent"
+              :label="$t('settings.links')"
+              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
+            />
+            <ContrastRatio :contrast="previewContrast.bgLink" />
+          </div>
+          <div class="color-item">
+            <ColorInput
+              v-model="fgColorLocal"
+              name="fgColor"
+              :label="$t('settings.foreground')"
+            />
+            <ColorInput
+              v-model="fgTextColorLocal"
+              name="fgTextColor"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.fgText"
+            />
+            <ColorInput
+              v-model="fgLinkColorLocal"
+              name="fgLinkColor"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.fgLink"
+            />
+            <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
+          </div>
+          <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
+          <div class="color-item">
+            <ColorInput
+              v-model="cRedColorLocal"
+              name="cRedColor"
+              :label="$t('settings.cRed')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCRed" />
+            <ColorInput
+              v-model="cBlueColorLocal"
+              name="cBlueColor"
+              :label="$t('settings.cBlue')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCBlue" />
+          </div>
+          <div class="color-item">
+            <ColorInput
+              v-model="cGreenColorLocal"
+              name="cGreenColor"
+              :label="$t('settings.cGreen')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCGreen" />
+            <ColorInput
+              v-model="cOrangeColorLocal"
+              name="cOrangeColor"
+              :label="$t('settings.cOrange')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCOrange" />
+          </div>
+          <p>{{ $t('settings.theme_help_v2_2') }}</p>
+        </div>
+
+        <div
+          :label="$t('settings.style.advanced_colors._tab_label')"
+          class="color-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.theme_help') }}</p>
+            <button
+              class="btn"
+              @click="clearOpacity"
+            >
+              {{ $t('settings.style.switcher.clear_opacity') }}
+            </button>
+            <button
+              class="btn"
+              @click="clearV1"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
+            <ColorInput
+              v-model="postLinkColorLocal"
+              name="postLinkColor"
+              :fallback="previewTheme.colors.accent"
+              :label="$t('settings.links')"
+            />
+            <ContrastRatio :contrast="previewContrast.postLink" />
+            <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
+            <ColorInput
+              v-model="alertErrorColorLocal"
+              name="alertError"
+              :label="$t('settings.style.advanced_colors.alert_error')"
+              :fallback="previewTheme.colors.alertError"
+            />
+            <ColorInput
+              v-model="alertErrorTextColorLocal"
+              name="alertErrorText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.alertErrorText"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.alertErrorText"
+              large="true"
+            />
+            <ColorInput
+              v-model="alertWarningColorLocal"
+              name="alertWarning"
+              :label="$t('settings.style.advanced_colors.alert_warning')"
+              :fallback="previewTheme.colors.alertWarning"
+            />
+            <ColorInput
+              v-model="alertWarningTextColorLocal"
+              name="alertWarningText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.alertWarningText"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.alertWarningText"
+              large="true"
+            />
+            <ColorInput
+              v-model="alertNeutralColorLocal"
+              name="alertNeutral"
+              :label="$t('settings.style.advanced_colors.alert_neutral')"
+              :fallback="previewTheme.colors.alertNeutral"
+            />
+            <ColorInput
+              v-model="alertNeutralTextColorLocal"
+              name="alertNeutralText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.alertNeutralText"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.alertNeutralText"
+              large="true"
+            />
+            <OpacityInput
+              v-model="alertOpacityLocal"
+              name="alertOpacity"
+              :fallback="previewTheme.opacity.alert"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
+            <ColorInput
+              v-model="badgeNotificationColorLocal"
+              name="badgeNotification"
+              :label="$t('settings.style.advanced_colors.badge_notification')"
+              :fallback="previewTheme.colors.badgeNotification"
+            />
+            <ColorInput
+              v-model="badgeNotificationTextColorLocal"
+              name="badgeNotificationText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.badgeNotificationText"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.badgeNotificationText"
+              large="true"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
+            <ColorInput
+              v-model="panelColorLocal"
+              name="panelColor"
+              :fallback="previewTheme.colors.panel"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="panelOpacityLocal"
+              name="panelOpacity"
+              :fallback="previewTheme.opacity.panel"
+              :disabled="panelColorLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="panelTextColorLocal"
+              name="panelTextColor"
+              :fallback="previewTheme.colors.panelText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.panelText"
+              large="true"
+            />
+            <ColorInput
+              v-model="panelLinkColorLocal"
+              name="panelLinkColor"
+              :fallback="previewTheme.colors.panelLink"
+              :label="$t('settings.links')"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.panelLink"
+              large="true"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
+            <ColorInput
+              v-model="topBarColorLocal"
+              name="topBarColor"
+              :fallback="previewTheme.colors.topBar"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="topBarTextColorLocal"
+              name="topBarTextColor"
+              :fallback="previewTheme.colors.topBarText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.topBarText" />
+            <ColorInput
+              v-model="topBarLinkColorLocal"
+              name="topBarLinkColor"
+              :fallback="previewTheme.colors.topBarLink"
+              :label="$t('settings.links')"
+            />
+            <ContrastRatio :contrast="previewContrast.topBarLink" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
+            <ColorInput
+              v-model="inputColorLocal"
+              name="inputColor"
+              :fallback="previewTheme.colors.input"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="inputOpacityLocal"
+              name="inputOpacity"
+              :fallback="previewTheme.opacity.input"
+              :disabled="inputColorLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="inputTextColorLocal"
+              name="inputTextColor"
+              :fallback="previewTheme.colors.inputText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.inputText" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
+            <ColorInput
+              v-model="btnColorLocal"
+              name="btnColor"
+              :fallback="previewTheme.colors.btn"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="btnOpacityLocal"
+              name="btnOpacity"
+              :fallback="previewTheme.opacity.btn"
+              :disabled="btnColorLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="btnTextColorLocal"
+              name="btnTextColor"
+              :fallback="previewTheme.colors.btnText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnText" />
+            <ColorInput
+              v-model="btnPanelTextColorLocal"
+              name="btnPanelTextColor"
+              :fallback="previewTheme.colors.btnPanelText"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnPanelText" />
+            <ColorInput
+              v-model="btnTopBarTextColorLocal"
+              name="btnTopBarTextColor"
+              :fallback="previewTheme.colors.btnTopBarText"
+              :label="$t('settings.style.advanced_colors.top_bar')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnTopBarText" />
+            <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
+            <ColorInput
+              v-model="btnPressedColorLocal"
+              name="btnPressedColor"
+              :fallback="previewTheme.colors.btnPressed"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="btnPressedTextColorLocal"
+              name="btnPressedTextColor"
+              :fallback="previewTheme.colors.btnPressedText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnPressedText" />
+            <ColorInput
+              v-model="btnPressedPanelTextColorLocal"
+              name="btnPressedPanelTextColor"
+              :fallback="previewTheme.colors.btnPressedPanelText"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
+            <ColorInput
+              v-model="btnPressedTopBarTextColorLocal"
+              name="btnPressedTopBarTextColor"
+              :fallback="previewTheme.colors.btnPressedTopBarText"
+              :label="$t('settings.style.advanced_colors.top_bar')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
+            <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
+            <ColorInput
+              v-model="btnDisabledColorLocal"
+              name="btnDisabledColor"
+              :fallback="previewTheme.colors.btnDisabled"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="btnDisabledTextColorLocal"
+              name="btnDisabledTextColor"
+              :fallback="previewTheme.colors.btnDisabledText"
+              :label="$t('settings.text')"
+            />
+            <ColorInput
+              v-model="btnDisabledPanelTextColorLocal"
+              name="btnDisabledPanelTextColor"
+              :fallback="previewTheme.colors.btnDisabledPanelText"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <ColorInput
+              v-model="btnDisabledTopBarTextColorLocal"
+              name="btnDisabledTopBarTextColor"
+              :fallback="previewTheme.colors.btnDisabledTopBarText"
+              :label="$t('settings.style.advanced_colors.top_bar')"
+            />
+            <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
+            <ColorInput
+              v-model="btnToggledColorLocal"
+              name="btnToggledColor"
+              :fallback="previewTheme.colors.btnToggled"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="btnToggledTextColorLocal"
+              name="btnToggledTextColor"
+              :fallback="previewTheme.colors.btnToggledText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnToggledText" />
+            <ColorInput
+              v-model="btnToggledPanelTextColorLocal"
+              name="btnToggledPanelTextColor"
+              :fallback="previewTheme.colors.btnToggledPanelText"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
+            <ColorInput
+              v-model="btnToggledTopBarTextColorLocal"
+              name="btnToggledTopBarTextColor"
+              :fallback="previewTheme.colors.btnToggledTopBarText"
+              :label="$t('settings.style.advanced_colors.top_bar')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
+            <ColorInput
+              v-model="tabColorLocal"
+              name="tabColor"
+              :fallback="previewTheme.colors.tab"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="tabTextColorLocal"
+              name="tabTextColor"
+              :fallback="previewTheme.colors.tabText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.tabText" />
+            <ColorInput
+              v-model="tabActiveTextColorLocal"
+              name="tabActiveTextColor"
+              :fallback="previewTheme.colors.tabActiveText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.tabActiveText" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
+            <ColorInput
+              v-model="borderColorLocal"
+              name="borderColor"
+              :fallback="previewTheme.colors.border"
+              :label="$t('settings.style.common.color')"
+            />
+            <OpacityInput
+              v-model="borderOpacityLocal"
+              name="borderOpacity"
+              :fallback="previewTheme.opacity.border"
+              :disabled="borderColorLocal === 'transparent'"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
+            <ColorInput
+              v-model="faintColorLocal"
+              name="faintColor"
+              :fallback="previewTheme.colors.faint"
+              :label="$t('settings.text')"
+            />
+            <ColorInput
+              v-model="faintLinkColorLocal"
+              name="faintLinkColor"
+              :fallback="previewTheme.colors.faintLink"
+              :label="$t('settings.links')"
+            />
+            <ColorInput
+              v-model="panelFaintColorLocal"
+              name="panelFaintColor"
+              :fallback="previewTheme.colors.panelFaint"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <OpacityInput
+              v-model="faintOpacityLocal"
+              name="faintOpacity"
+              :fallback="previewTheme.opacity.faint"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
+            <ColorInput
+              v-model="underlayColorLocal"
+              name="underlay"
+              :label="$t('settings.style.advanced_colors.underlay')"
+              :fallback="previewTheme.colors.underlay"
+            />
+            <OpacityInput
+              v-model="underlayOpacityLocal"
+              name="underlayOpacity"
+              :fallback="previewTheme.opacity.underlay"
+              :disabled="underlayOpacityLocal === 'transparent'"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
+            <ColorInput
+              v-model="pollColorLocal"
+              name="poll"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.poll"
+            />
+            <ColorInput
+              v-model="pollTextColorLocal"
+              name="pollText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.pollText"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
+            <ColorInput
+              v-model="iconColorLocal"
+              name="icon"
+              :label="$t('settings.style.advanced_colors.icons')"
+              :fallback="previewTheme.colors.icon"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
+            <ColorInput
+              v-model="highlightColorLocal"
+              name="highlight"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.highlight"
+            />
+            <ColorInput
+              v-model="highlightTextColorLocal"
+              name="highlightText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.highlightText"
+            />
+            <ContrastRatio :contrast="previewContrast.highlightText" />
+            <ColorInput
+              v-model="highlightLinkColorLocal"
+              name="highlightLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.highlightLink"
+            />
+            <ContrastRatio :contrast="previewContrast.highlightLink" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
+            <ColorInput
+              v-model="popoverColorLocal"
+              name="popover"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.popover"
+            />
+            <OpacityInput
+              v-model="popoverOpacityLocal"
+              name="popoverOpacity"
+              :fallback="previewTheme.opacity.popover"
+              :disabled="popoverOpacityLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="popoverTextColorLocal"
+              name="popoverText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.popoverText"
+            />
+            <ContrastRatio :contrast="previewContrast.popoverText" />
+            <ColorInput
+              v-model="popoverLinkColorLocal"
+              name="popoverLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.popoverLink"
+            />
+            <ContrastRatio :contrast="previewContrast.popoverLink" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
+            <ColorInput
+              v-model="selectedPostColorLocal"
+              name="selectedPost"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.selectedPost"
+            />
+            <ColorInput
+              v-model="selectedPostTextColorLocal"
+              name="selectedPostText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.selectedPostText"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedPostText" />
+            <ColorInput
+              v-model="selectedPostLinkColorLocal"
+              name="selectedPostLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.selectedPostLink"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedPostLink" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
+            <ColorInput
+              v-model="selectedMenuColorLocal"
+              name="selectedMenu"
+              :label="$t('settings.background')"
+              :fallback="previewTheme.colors.selectedMenu"
+            />
+            <ColorInput
+              v-model="selectedMenuTextColorLocal"
+              name="selectedMenuText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.selectedMenuText"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedMenuText" />
+            <ColorInput
+              v-model="selectedMenuLinkColorLocal"
+              name="selectedMenuLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.selectedMenuLink"
+            />
+            <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
+          </div>
+        </div>
+
+        <div
+          :label="$t('settings.style.radii._tab_label')"
+          class="radius-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.radii_help') }}</p>
+            <button
+              class="btn"
+              @click="clearRoundness"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <RangeInput
+            v-model="btnRadiusLocal"
+            name="btnRadius"
+            :label="$t('settings.btnRadius')"
+            :fallback="previewTheme.radii.btn"
+            max="16"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="inputRadiusLocal"
+            name="inputRadius"
+            :label="$t('settings.inputRadius')"
+            :fallback="previewTheme.radii.input"
+            max="9"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="checkboxRadiusLocal"
+            name="checkboxRadius"
+            :label="$t('settings.checkboxRadius')"
+            :fallback="previewTheme.radii.checkbox"
+            max="16"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="panelRadiusLocal"
+            name="panelRadius"
+            :label="$t('settings.panelRadius')"
+            :fallback="previewTheme.radii.panel"
+            max="50"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="avatarRadiusLocal"
+            name="avatarRadius"
+            :label="$t('settings.avatarRadius')"
+            :fallback="previewTheme.radii.avatar"
+            max="28"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="avatarAltRadiusLocal"
+            name="avatarAltRadius"
+            :label="$t('settings.avatarAltRadius')"
+            :fallback="previewTheme.radii.avatarAlt"
+            max="28"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="attachmentRadiusLocal"
+            name="attachmentRadius"
+            :label="$t('settings.attachmentRadius')"
+            :fallback="previewTheme.radii.attachment"
+            max="50"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="tooltipRadiusLocal"
+            name="tooltipRadius"
+            :label="$t('settings.tooltipRadius')"
+            :fallback="previewTheme.radii.tooltip"
+            max="50"
+            hard-min="0"
+          />
+        </div>
+
+        <div
+          :label="$t('settings.style.shadows._tab_label')"
+          class="shadow-container"
+        >
+          <div class="tab-header shadow-selector">
+            <div class="select-container">
+              {{ $t('settings.style.shadows.component') }}
+              <label
+                for="shadow-switcher"
+                class="select"
+              >
+                <select
+                  id="shadow-switcher"
+                  v-model="shadowSelected"
+                  class="shadow-switcher"
+                >
+                  <option
+                    v-for="shadow in shadowsAvailable"
+                    :key="shadow"
+                    :value="shadow"
+                  >
+                    {{ $t('settings.style.shadows.components.' + shadow) }}
+                  </option>
+                </select>
+                <i class="icon-down-open" />
+              </label>
+            </div>
+            <div class="override">
+              <label
+                for="override"
+                class="label"
+              >
+                {{ $t('settings.style.shadows.override') }}
+              </label>
+              <input
+                id="override"
+                v-model="currentShadowOverriden"
+                name="override"
+                class="input-override"
+                type="checkbox"
+              >
+              <label
+                class="checkbox-label"
+                for="override"
+              />
+            </div>
+            <button
+              class="btn"
+              @click="clearShadows"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <ShadowControl
+            v-model="currentShadow"
+            :ready="!!currentShadowFallback"
+            :fallback="currentShadowFallback"
+          />
+          <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
+            <i18n
+              path="settings.style.shadows.filter_hint.always_drop_shadow"
+              tag="p"
+            >
+              <code>filter: drop-shadow()</code>
+            </i18n>
+            <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
+            <i18n
+              path="settings.style.shadows.filter_hint.drop_shadow_syntax"
+              tag="p"
+            >
+              <code>drop-shadow</code>
+              <code>spread-radius</code>
+              <code>inset</code>
+            </i18n>
+            <i18n
+              path="settings.style.shadows.filter_hint.inset_classic"
+              tag="p"
+            >
+              <code>box-shadow</code>
+            </i18n>
+            <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
+          </div>
+        </div>
+
+        <div
+          :label="$t('settings.style.fonts._tab_label')"
+          class="fonts-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.style.fonts.help') }}</p>
+            <button
+              class="btn"
+              @click="clearFonts"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <FontControl
+            v-model="fontsLocal.interface"
+            name="ui"
+            :label="$t('settings.style.fonts.components.interface')"
+            :fallback="previewTheme.fonts.interface"
+            no-inherit="1"
+          />
+          <FontControl
+            v-model="fontsLocal.input"
+            name="input"
+            :label="$t('settings.style.fonts.components.input')"
+            :fallback="previewTheme.fonts.input"
+          />
+          <FontControl
+            v-model="fontsLocal.post"
+            name="post"
+            :label="$t('settings.style.fonts.components.post')"
+            :fallback="previewTheme.fonts.post"
+          />
+          <FontControl
+            v-model="fontsLocal.postCode"
+            name="postCode"
+            :label="$t('settings.style.fonts.components.postCode')"
+            :fallback="previewTheme.fonts.postCode"
+          />
+        </div>
+      </tab-switcher>
+    </keep-alive>
+
+    <div class="apply-container">
+      <button
+        class="btn submit"
+        :disabled="!themeValid"
+        @click="setCustomTheme"
+      >
+        {{ $t('general.apply') }}
+      </button>
+      <button
+        class="btn"
+        @click="clearAll"
+      >
+        {{ $t('settings.style.switcher.reset') }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script src="./theme_tab.js"></script>
+
+<style src="./theme_tab.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/tabs/version_tab.js b/src/components/settings_modal/tabs/version_tab.js
new file mode 100644
index 00000000..616bdadf
--- /dev/null
+++ b/src/components/settings_modal/tabs/version_tab.js
@@ -0,0 +1,24 @@
+import { extractCommit } from 'src/services/version/version.service'
+
+const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
+const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
+
+const VersionTab = {
+  data () {
+    const instance = this.$store.state.instance
+    return {
+      backendVersion: instance.backendVersion,
+      frontendVersion: instance.frontendVersion
+    }
+  },
+  computed: {
+    frontendVersionLink () {
+      return pleromaFeCommitUrl + this.frontendVersion
+    },
+    backendVersionLink () {
+      return pleromaBeCommitUrl + extractCommit(this.backendVersion)
+    }
+  }
+}
+
+export default VersionTab
diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue
new file mode 100644
index 00000000..acc43569
--- /dev/null
+++ b/src/components/settings_modal/tabs/version_tab.vue
@@ -0,0 +1,31 @@
+<template>
+<div :label="$t('settings.version.title')">
+  <div class="setting-item">
+    <ul class="setting-list">
+      <li>
+        <p>{{ $t('settings.version.backend_version') }}</p>
+        <ul class="option-list">
+          <li>
+            <a
+              :href="backendVersionLink"
+              target="_blank"
+              >{{ backendVersion }}</a>
+          </li>
+        </ul>
+      </li>
+      <li>
+        <p>{{ $t('settings.version.frontend_version') }}</p>
+        <ul class="option-list">
+          <li>
+            <a
+              :href="frontendVersionLink"
+              target="_blank"
+              >{{ frontendVersion }}</a>
+          </li>
+        </ul>
+      </li>
+    </ul>
+  </div>
+</div>
+</template>
+<script src="./version_tab.js">
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
deleted file mode 100644
index 9d984659..00000000
--- a/src/components/style_switcher/preview.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
-  <div class="preview-container">
-    <div class="underlay underlay-preview" />
-    <div class="panel dummy">
-      <div class="panel-heading">
-        <div class="title">
-          {{ $t('settings.style.preview.header') }}
-          <span class="badge badge-notification">
-            99
-          </span>
-        </div>
-        <span class="faint">
-          {{ $t('settings.style.preview.header_faint') }}
-        </span>
-        <span class="alert error">
-          {{ $t('settings.style.preview.error') }}
-        </span>
-        <button class="btn">
-          {{ $t('settings.style.preview.button') }}
-        </button>
-      </div>
-      <div class="panel-body theme-preview-content">
-        <div class="post">
-          <div class="avatar still-image">
-            ( ͡° ͜ʖ ͡°)
-          </div>
-          <div class="content">
-            <h4>
-              {{ $t('settings.style.preview.content') }}
-            </h4>
-
-            <i18n path="settings.style.preview.text">
-              <code style="font-family: var(--postCodeFont)">
-                {{ $t('settings.style.preview.mono') }}
-              </code>
-              <a style="color: var(--link)">
-                {{ $t('settings.style.preview.link') }}
-              </a>
-            </i18n>
-
-            <div class="icons">
-              <i
-                style="color: var(--cBlue)"
-                class="button-icon icon-reply"
-              />
-              <i
-                style="color: var(--cGreen)"
-                class="button-icon icon-retweet"
-              />
-              <i
-                style="color: var(--cOrange)"
-                class="button-icon icon-star"
-              />
-              <i
-                style="color: var(--cRed)"
-                class="button-icon icon-cancel"
-              />
-            </div>
-          </div>
-        </div>
-
-        <div class="after-post">
-          <div class="avatar-alt">
-            :^)
-          </div>
-          <div class="content">
-            <i18n
-              path="settings.style.preview.fine_print"
-              tag="span"
-              class="faint"
-            >
-              <a style="color: var(--faintLink)">
-                {{ $t('settings.style.preview.faint_link') }}
-              </a>
-            </i18n>
-          </div>
-        </div>
-        <div class="separator" />
-
-        <span class="alert error">
-          {{ $t('settings.style.preview.error') }}
-        </span>
-        <input
-          :value="$t('settings.style.preview.input')"
-          type="text"
-        >
-
-        <div class="actions">
-          <span class="checkbox">
-            <input
-              id="preview_checkbox"
-              checked="very yes"
-              type="checkbox"
-            >
-            <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
-          </span>
-          <button class="btn">
-            {{ $t('settings.style.preview.button') }}
-          </button>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss">
-.preview-container {
-  position: relative;
-}
-.underlay-preview {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  left: 10px;
-  right: 10px;
-}
-</style>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
deleted file mode 100644
index a7f586f4..00000000
--- a/src/components/style_switcher/style_switcher.js
+++ /dev/null
@@ -1,758 +0,0 @@
-import { set, delete as del } from 'vue'
-import {
-  rgb2hex,
-  hex2rgb,
-  getContrastRatioLayers
-} from '../../services/color_convert/color_convert.js'
-import {
-  DEFAULT_SHADOWS,
-  generateColors,
-  generateShadows,
-  generateRadii,
-  generateFonts,
-  composePreset,
-  getThemes,
-  shadows2to3,
-  colors2to3
-} from '../../services/style_setter/style_setter.js'
-import {
-  SLOT_INHERITANCE
-} from '../../services/theme_data/pleromafe.js'
-import {
-  CURRENT_VERSION,
-  OPACITIES,
-  getLayers,
-  getOpacitySlot
-} from '../../services/theme_data/theme_data.service.js'
-import ColorInput from '../color_input/color_input.vue'
-import RangeInput from '../range_input/range_input.vue'
-import OpacityInput from '../opacity_input/opacity_input.vue'
-import ShadowControl from '../shadow_control/shadow_control.vue'
-import FontControl from '../font_control/font_control.vue'
-import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import Preview from './preview.vue'
-import ExportImport from '../export_import/export_import.vue'
-import Checkbox from '../checkbox/checkbox.vue'
-
-// List of color values used in v1
-const v1OnlyNames = [
-  'bg',
-  'fg',
-  'text',
-  'link',
-  'cRed',
-  'cGreen',
-  'cBlue',
-  'cOrange'
-].map(_ => _ + 'ColorLocal')
-
-const colorConvert = (color) => {
-  if (color.startsWith('--') || color === 'transparent') {
-    return color
-  } else {
-    return hex2rgb(color)
-  }
-}
-
-export default {
-  data () {
-    return {
-      availableStyles: [],
-      selected: this.$store.getters.mergedConfig.theme,
-      themeWarning: undefined,
-      tempImportFile: undefined,
-      engineVersion: 0,
-
-      previewShadows: {},
-      previewColors: {},
-      previewRadii: {},
-      previewFonts: {},
-
-      shadowsInvalid: true,
-      colorsInvalid: true,
-      radiiInvalid: true,
-
-      keepColor: false,
-      keepShadows: false,
-      keepOpacity: false,
-      keepRoundness: false,
-      keepFonts: false,
-
-      ...Object.keys(SLOT_INHERITANCE)
-        .map(key => [key, ''])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
-
-      ...Object.keys(OPACITIES)
-        .map(key => [key, ''])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
-
-      shadowSelected: undefined,
-      shadowsLocal: {},
-      fontsLocal: {},
-
-      btnRadiusLocal: '',
-      inputRadiusLocal: '',
-      checkboxRadiusLocal: '',
-      panelRadiusLocal: '',
-      avatarRadiusLocal: '',
-      avatarAltRadiusLocal: '',
-      attachmentRadiusLocal: '',
-      tooltipRadiusLocal: ''
-    }
-  },
-  created () {
-    const self = this
-
-    getThemes()
-      .then((promises) => {
-        return Promise.all(
-          Object.entries(promises)
-            .map(([k, v]) => v.then(res => [k, res]))
-        )
-      })
-      .then(themes => themes.reduce((acc, [k, v]) => {
-        if (v) {
-          return {
-            ...acc,
-            [k]: v
-          }
-        } else {
-          return acc
-        }
-      }, {}))
-      .then((themesComplete) => {
-        self.availableStyles = themesComplete
-      })
-  },
-  mounted () {
-    this.loadThemeFromLocalStorage()
-    if (typeof this.shadowSelected === 'undefined') {
-      this.shadowSelected = this.shadowsAvailable[0]
-    }
-  },
-  computed: {
-    themeWarningHelp () {
-      if (!this.themeWarning) return
-      const t = this.$t
-      const pre = 'settings.style.switcher.help.'
-      const {
-        origin,
-        themeEngineVersion,
-        type,
-        noActionsPossible
-      } = this.themeWarning
-      if (origin === 'file') {
-        // Loaded v2 theme from file
-        if (themeEngineVersion === 2 && type === 'wrong_version') {
-          return t(pre + 'v2_imported')
-        }
-        if (themeEngineVersion > CURRENT_VERSION) {
-          return t(pre + 'future_version_imported') + ' ' +
-            (
-              noActionsPossible
-                ? t(pre + 'snapshot_missing')
-                : t(pre + 'snapshot_present')
-            )
-        }
-        if (themeEngineVersion < CURRENT_VERSION) {
-          return t(pre + 'future_version_imported') + ' ' +
-            (
-              noActionsPossible
-                ? t(pre + 'snapshot_missing')
-                : t(pre + 'snapshot_present')
-            )
-        }
-      } else if (origin === 'localStorage') {
-        if (type === 'snapshot_source_mismatch') {
-          return t(pre + 'snapshot_source_mismatch')
-        }
-        // FE upgraded from v2
-        if (themeEngineVersion === 2) {
-          return t(pre + 'upgraded_from_v2')
-        }
-        // Admin downgraded FE
-        if (themeEngineVersion > CURRENT_VERSION) {
-          return t(pre + 'fe_downgraded') + ' ' +
-            (
-              noActionsPossible
-                ? t(pre + 'migration_snapshot_ok')
-                : t(pre + 'migration_snapshot_gone')
-            )
-        }
-        // Admin upgraded FE
-        if (themeEngineVersion < CURRENT_VERSION) {
-          return t(pre + 'fe_upgraded') + ' ' +
-            (
-              noActionsPossible
-                ? t(pre + 'migration_snapshot_ok')
-                : t(pre + 'migration_snapshot_gone')
-            )
-        }
-      }
-    },
-    selectedVersion () {
-      return Array.isArray(this.selected) ? 1 : 2
-    },
-    currentColors () {
-      return Object.keys(SLOT_INHERITANCE)
-        .map(key => [key, this[key + 'ColorLocal']])
-        .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 }), {})
-    },
-    currentRadii () {
-      return {
-        btn: this.btnRadiusLocal,
-        input: this.inputRadiusLocal,
-        checkbox: this.checkboxRadiusLocal,
-        panel: this.panelRadiusLocal,
-        avatar: this.avatarRadiusLocal,
-        avatarAlt: this.avatarAltRadiusLocal,
-        tooltip: this.tooltipRadiusLocal,
-        attachment: this.attachmentRadiusLocal
-      }
-    },
-    preview () {
-      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
-    },
-    previewTheme () {
-      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
-      return this.preview.theme
-    },
-    // This needs optimization maybe
-    previewContrast () {
-      try {
-        if (!this.previewTheme.colors.bg) return {}
-        const colors = this.previewTheme.colors
-        const opacity = this.previewTheme.opacity
-        if (!colors.bg) return {}
-        const hints = (ratio) => ({
-          text: ratio.toPrecision(3) + ':1',
-          // AA level, AAA level
-          aa: ratio >= 4.5,
-          aaa: ratio >= 7,
-          // same but for 18pt+ texts
-          laa: ratio >= 3,
-          laaa: ratio >= 4.5
-        })
-        const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
-
-        const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
-          const slotIsBaseText = key === 'text' || key === 'link'
-          const slotIsText = slotIsBaseText || (
-            typeof value === 'object' && value !== null && value.textColor
-          )
-          if (!slotIsText) return acc
-          const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
-          const background = variant || layer
-          const opacitySlot = getOpacitySlot(background)
-          const textColors = [
-            key,
-            ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
-          ]
-
-          const layers = getLayers(
-            layer,
-            variant || layer,
-            opacitySlot,
-            colorsConverted,
-            opacity
-          )
-
-          return {
-            ...acc,
-            ...textColors.reduce((acc, textColorKey) => {
-              const newKey = slotIsBaseText
-                ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
-                : textColorKey
-              return {
-                ...acc,
-                [newKey]: getContrastRatioLayers(
-                  colorsConverted[textColorKey],
-                  layers,
-                  colorsConverted[textColorKey]
-                )
-              }
-            }, {})
-          }
-        }, {})
-
-        return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
-      } catch (e) {
-        console.warn('Failure computing contrasts', e)
-      }
-    },
-    previewRules () {
-      if (!this.preview.rules) return ''
-      return [
-        ...Object.values(this.preview.rules),
-        'color: var(--text)',
-        'font-family: var(--interfaceFont, sans-serif)'
-      ].join(';')
-    },
-    shadowsAvailable () {
-      return Object.keys(DEFAULT_SHADOWS).sort()
-    },
-    currentShadowOverriden: {
-      get () {
-        return !!this.currentShadow
-      },
-      set (val) {
-        if (val) {
-          set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
-        } else {
-          del(this.shadowsLocal, this.shadowSelected)
-        }
-      }
-    },
-    currentShadowFallback () {
-      return (this.previewTheme.shadows || {})[this.shadowSelected]
-    },
-    currentShadow: {
-      get () {
-        return this.shadowsLocal[this.shadowSelected]
-      },
-      set (v) {
-        set(this.shadowsLocal, this.shadowSelected, v)
-      }
-    },
-    themeValid () {
-      return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
-    },
-    exportedTheme () {
-      const saveEverything = (
-        !this.keepFonts &&
-        !this.keepShadows &&
-        !this.keepOpacity &&
-        !this.keepRoundness &&
-        !this.keepColor
-      )
-
-      const source = {
-        themeEngineVersion: CURRENT_VERSION
-      }
-
-      if (this.keepFonts || saveEverything) {
-        source.fonts = this.fontsLocal
-      }
-      if (this.keepShadows || saveEverything) {
-        source.shadows = this.shadowsLocal
-      }
-      if (this.keepOpacity || saveEverything) {
-        source.opacity = this.currentOpacity
-      }
-      if (this.keepColor || saveEverything) {
-        source.colors = this.currentColors
-      }
-      if (this.keepRoundness || saveEverything) {
-        source.radii = this.currentRadii
-      }
-
-      const theme = {
-        themeEngineVersion: CURRENT_VERSION,
-        ...this.previewTheme
-      }
-
-      return {
-        // To separate from other random JSON files and possible future source formats
-        _pleroma_theme_version: 2, theme, source
-      }
-    }
-  },
-  components: {
-    ColorInput,
-    OpacityInput,
-    RangeInput,
-    ContrastRatio,
-    ShadowControl,
-    FontControl,
-    TabSwitcher,
-    Preview,
-    ExportImport,
-    Checkbox
-  },
-  methods: {
-    loadTheme (
-      {
-        theme,
-        source,
-        _pleroma_theme_version: fileVersion
-      },
-      origin,
-      forceUseSource = false
-    ) {
-      this.dismissWarning()
-      if (!source && !theme) {
-        throw new Error('Can\'t load theme: empty')
-      }
-      const version = (origin === 'localStorage' && !theme.colors)
-        ? 'l1'
-        : fileVersion
-      const snapshotEngineVersion = (theme || {}).themeEngineVersion
-      const themeEngineVersion = (source || {}).themeEngineVersion || 2
-      const versionsMatch = themeEngineVersion === CURRENT_VERSION
-      const sourceSnapshotMismatch = (
-        theme !== undefined &&
-          source !== undefined &&
-          themeEngineVersion !== snapshotEngineVersion
-      )
-      // Force loading of source if user requested it or if snapshot
-      // is unavailable
-      const forcedSourceLoad = (source && forceUseSource) || !theme
-      if (!(versionsMatch && !sourceSnapshotMismatch) &&
-          !forcedSourceLoad &&
-          version !== 'l1' &&
-          origin !== 'defaults'
-      ) {
-        if (sourceSnapshotMismatch && origin === 'localStorage') {
-          this.themeWarning = {
-            origin,
-            themeEngineVersion,
-            type: 'snapshot_source_mismatch'
-          }
-        } else if (!theme) {
-          this.themeWarning = {
-            origin,
-            noActionsPossible: true,
-            themeEngineVersion,
-            type: 'no_snapshot_old_version'
-          }
-        } else if (!versionsMatch) {
-          this.themeWarning = {
-            origin,
-            noActionsPossible: !source,
-            themeEngineVersion,
-            type: 'wrong_version'
-          }
-        }
-      }
-      this.normalizeLocalState(theme, version, source, forcedSourceLoad)
-    },
-    forceLoadLocalStorage () {
-      this.loadThemeFromLocalStorage(true)
-    },
-    dismissWarning () {
-      this.themeWarning = undefined
-      this.tempImportFile = undefined
-    },
-    forceLoad () {
-      const { origin } = this.themeWarning
-      switch (origin) {
-        case 'localStorage':
-          this.loadThemeFromLocalStorage(true)
-          break
-        case 'file':
-          this.onImport(this.tempImportFile, true)
-          break
-      }
-      this.dismissWarning()
-    },
-    forceSnapshot () {
-      const { origin } = this.themeWarning
-      switch (origin) {
-        case 'localStorage':
-          this.loadThemeFromLocalStorage(false, true)
-          break
-        case 'file':
-          console.err('Forcing snapshout from file is not supported yet')
-          break
-      }
-      this.dismissWarning()
-    },
-    loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
-      const {
-        customTheme: theme,
-        customThemeSource: source
-      } = this.$store.getters.mergedConfig
-      if (!theme && !source) {
-        // Anon user or never touched themes
-        this.loadTheme(
-          this.$store.state.instance.themeData,
-          'defaults',
-          confirmLoadSource
-        )
-      } else {
-        this.loadTheme(
-          {
-            theme,
-            source: forceSnapshot ? theme : source
-          },
-          'localStorage',
-          confirmLoadSource
-        )
-      }
-    },
-    setCustomTheme () {
-      this.$store.dispatch('setOption', {
-        name: 'customTheme',
-        value: {
-          themeEngineVersion: CURRENT_VERSION,
-          ...this.previewTheme
-        }
-      })
-      this.$store.dispatch('setOption', {
-        name: 'customThemeSource',
-        value: {
-          themeEngineVersion: CURRENT_VERSION,
-          shadows: this.shadowsLocal,
-          fonts: this.fontsLocal,
-          opacity: this.currentOpacity,
-          colors: this.currentColors,
-          radii: this.currentRadii
-        }
-      })
-    },
-    updatePreviewColorsAndShadows () {
-      this.previewColors = generateColors({
-        opacity: this.currentOpacity,
-        colors: this.currentColors
-      })
-      this.previewShadows = generateShadows(
-        { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
-        this.previewColors.theme.colors,
-        this.previewColors.mod
-      )
-    },
-    onImport (parsed, forceSource = false) {
-      this.tempImportFile = parsed
-      this.loadTheme(parsed, 'file', forceSource)
-    },
-    importValidator (parsed) {
-      const version = parsed._pleroma_theme_version
-      return version >= 1 || version <= 2
-    },
-    clearAll () {
-      this.loadThemeFromLocalStorage()
-    },
-
-    // Clears all the extra stuff when loading V1 theme
-    clearV1 () {
-      Object.keys(this.$data)
-        .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
-        .filter(_ => !v1OnlyNames.includes(_))
-        .forEach(key => {
-          set(this.$data, key, undefined)
-        })
-    },
-
-    clearRoundness () {
-      Object.keys(this.$data)
-        .filter(_ => _.endsWith('RadiusLocal'))
-        .forEach(key => {
-          set(this.$data, key, undefined)
-        })
-    },
-
-    clearOpacity () {
-      Object.keys(this.$data)
-        .filter(_ => _.endsWith('OpacityLocal'))
-        .forEach(key => {
-          set(this.$data, key, undefined)
-        })
-    },
-
-    clearShadows () {
-      this.shadowsLocal = {}
-    },
-
-    clearFonts () {
-      this.fontsLocal = {}
-    },
-
-    /**
-     * This applies stored theme data onto form. Supports three versions of data:
-     * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
-     * v2 (version = 2) - newer version of themes.
-     * v1 (version = 1) - older version of themes (import from file)
-     * v1l (version = l1) - older version of theme (load from local storage)
-     * v1 and v1l differ because of way themes were stored/exported.
-     * @param {Object} theme - theme data (snapshot)
-     * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
-     * @param {Object} source - theme source - this will be used if compatible
-     * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
-     *                           this allows importing source anyway
-     */
-    normalizeLocalState (theme, version = 0, source, forceSource = false) {
-      let input
-      if (typeof source !== 'undefined') {
-        if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
-          input = source
-          version = source.themeEngineVersion
-        } else {
-          input = theme
-        }
-      } else {
-        input = theme
-      }
-
-      const radii = input.radii || input
-      const opacity = input.opacity
-      const shadows = input.shadows || {}
-      const fonts = input.fonts || {}
-      const colors = !input.themeEngineVersion
-        ? colors2to3(input.colors || input)
-        : input.colors || input
-
-      if (version === 0) {
-        if (input.version) version = input.version
-        // Old v1 naming: fg is text, btn is foreground
-        if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
-          version = 1
-        }
-        // New v2 naming: text is text, fg is foreground
-        if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
-          version = 2
-        }
-      }
-
-      this.engineVersion = version
-
-      // Stuff that differs between V1 and V2
-      if (version === 1) {
-        this.fgColorLocal = rgb2hex(colors.btn)
-        this.textColorLocal = rgb2hex(colors.fg)
-      }
-
-      if (!this.keepColor) {
-        this.clearV1()
-        const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
-        if (version === 1 || version === 'l1') {
-          keys
-            .add('bg')
-            .add('link')
-            .add('cRed')
-            .add('cBlue')
-            .add('cGreen')
-            .add('cOrange')
-        }
-
-        keys.forEach(key => {
-          const color = colors[key]
-          const hex = rgb2hex(colors[key])
-          this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
-        })
-      }
-
-      if (opacity && !this.keepOpacity) {
-        this.clearOpacity()
-        Object.entries(opacity).forEach(([k, v]) => {
-          if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
-          this[k + 'OpacityLocal'] = v
-        })
-      }
-
-      if (!this.keepRoundness) {
-        this.clearRoundness()
-        Object.entries(radii).forEach(([k, v]) => {
-          // 'Radius' is kept mostly for v1->v2 localstorage transition
-          const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
-          this[key + 'RadiusLocal'] = v
-        })
-      }
-
-      if (!this.keepShadows) {
-        this.clearShadows()
-        if (version === 2) {
-          this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
-        } else {
-          this.shadowsLocal = shadows
-        }
-        this.shadowSelected = this.shadowsAvailable[0]
-      }
-
-      if (!this.keepFonts) {
-        this.clearFonts()
-        this.fontsLocal = fonts
-      }
-    }
-  },
-  watch: {
-    currentRadii () {
-      try {
-        this.previewRadii = generateRadii({ radii: this.currentRadii })
-        this.radiiInvalid = false
-      } catch (e) {
-        this.radiiInvalid = true
-        console.warn(e)
-      }
-    },
-    shadowsLocal: {
-      handler () {
-        if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
-        try {
-          this.updatePreviewColorsAndShadows()
-          this.shadowsInvalid = false
-        } catch (e) {
-          this.shadowsInvalid = true
-          console.warn(e)
-        }
-      },
-      deep: true
-    },
-    fontsLocal: {
-      handler () {
-        try {
-          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
-          this.fontsInvalid = false
-        } catch (e) {
-          this.fontsInvalid = true
-          console.warn(e)
-        }
-      },
-      deep: true
-    },
-    currentColors () {
-      try {
-        this.updatePreviewColorsAndShadows()
-        this.colorsInvalid = false
-        this.shadowsInvalid = false
-      } catch (e) {
-        this.colorsInvalid = true
-        this.shadowsInvalid = true
-        console.warn(e)
-      }
-    },
-    currentOpacity () {
-      try {
-        this.updatePreviewColorsAndShadows()
-      } catch (e) {
-        console.warn(e)
-      }
-    },
-    selected () {
-      this.dismissWarning()
-      if (this.selectedVersion === 1) {
-        if (!this.keepRoundness) {
-          this.clearRoundness()
-        }
-
-        if (!this.keepShadows) {
-          this.clearShadows()
-        }
-
-        if (!this.keepOpacity) {
-          this.clearOpacity()
-        }
-
-        if (!this.keepColor) {
-          this.clearV1()
-
-          this.bgColorLocal = this.selected[1]
-          this.fgColorLocal = this.selected[2]
-          this.textColorLocal = this.selected[3]
-          this.linkColorLocal = this.selected[4]
-          this.cRedColorLocal = this.selected[5]
-          this.cGreenColorLocal = this.selected[6]
-          this.cBlueColorLocal = this.selected[7]
-          this.cOrangeColorLocal = this.selected[8]
-        }
-      } else if (this.selectedVersion >= 2) {
-        this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
-      }
-    }
-  }
-}
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
deleted file mode 100644
index d2a40d13..00000000
--- a/src/components/style_switcher/style_switcher.scss
+++ /dev/null
@@ -1,335 +0,0 @@
-@import '../../_variables.scss';
-.style-switcher {
-  .theme-warning {
-    display: flex;
-    align-items: baseline;
-    margin-bottom: .5em;
-    .buttons {
-      .btn {
-        margin-bottom: .5em;
-      }
-    }
-  }
-  .preset-switcher {
-    margin-right: 1em;
-  }
-
-  .style-control {
-    display: flex;
-    align-items: baseline;
-    margin-bottom: 5px;
-
-    .label {
-      flex: 1;
-    }
-
-    &.disabled {
-      input, select {
-        opacity: .5
-      }
-    }
-
-    .opt {
-      margin: .5em;
-    }
-
-    .color-input {
-      flex: 0 0 0;
-    }
-
-    input, select {
-      min-width: 3em;
-      margin: 0;
-      flex: 0;
-
-      &[type=number] {
-        min-width: 5em;
-      }
-
-      &[type=range] {
-        flex: 1;
-        min-width: 3em;
-        align-self: flex-start;
-      }
-    }
-  }
-
-  .tab-switcher {
-    margin: 0 -1em;
-  }
-
-  .reset-container {
-    flex-wrap: wrap;
-  }
-
-  .fonts-container,
-  .reset-container,
-  .apply-container,
-  .radius-container,
-  .color-container,
-  {
-    display: flex;
-  }
-
-  .fonts-container,
-  .radius-container {
-    flex-direction: column;
-  }
-
-  .color-container{
-    > h4 {
-      width: 99%;
-    }
-    flex-wrap: wrap;
-    justify-content: space-between;
-  }
-
-  .fonts-container,
-  .color-container,
-  .shadow-container,
-  .radius-container,
-  .presets-container {
-    margin: 1em 1em 0;
-  }
-
-  .tab-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: baseline;
-    width: 100%;
-    min-height: 30px;
-
-    .btn {
-      min-width: 1px;
-      flex: 0 auto;
-      padding: 0 1em;
-    }
-
-    p {
-      flex: 1;
-      margin: 0;
-      margin-right: .5em;
-    }
-
-    margin-bottom: 1em;
-  }
-
-  .shadow-selector {
-    .override {
-      flex: 1;
-      margin-left: .5em;
-    }
-    .select-container {
-      margin-top: -4px;
-      margin-bottom: -3px;
-    }
-  }
-
-  .save-load,
-  .save-load-options {
-    display: flex;
-    justify-content: center;
-    align-items: baseline;
-    flex-wrap: wrap;
-
-    .presets,
-    .import-export {
-      margin-bottom: .5em;
-    }
-
-    .import-export {
-      display: flex;
-    }
-
-    .override {
-      margin-left: .5em;
-    }
-  }
-
-  .save-load-options {
-    flex-wrap: wrap;
-    margin-top: .5em;
-    justify-content: center;
-    .keep-option {
-      margin: 0 .5em .5em;
-      min-width: 25%;
-    }
-  }
-
-  .preview-container {
-    border-top: 1px dashed;
-    border-bottom: 1px dashed;
-    border-color: $fallback--border;
-    border-color: var(--border, $fallback--border);
-    margin: 1em -1em 0;
-    padding: 1em;
-    background: var(--body-background-image);
-    background-size: cover;
-    background-position: 50% 50%;
-
-    .dummy {
-      .post {
-        font-family: var(--postFont);
-        display: flex;
-
-        .content {
-          flex: 1;
-
-          h4 {
-            margin-bottom: .25em;
-          }
-
-          .icons {
-            margin-top: .5em;
-            display: flex;
-
-            i {
-              margin-right: 1em;
-            }
-          }
-        }
-      }
-
-      .after-post {
-        margin-top: 1em;
-        display: flex;
-        align-items: center;
-      }
-
-      .avatar, .avatar-alt{
-        background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
-        color: black;
-        font-family: sans-serif;
-        text-align: center;
-        margin-right: 1em;
-      }
-
-      .avatar-alt {
-        flex: 0 auto;
-        margin-left: 28px;
-        font-size: 12px;
-        min-width: 20px;
-        min-height: 20px;
-        line-height: 20px;
-        border-radius: $fallback--avatarAltRadius;
-        border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
-      }
-
-      .avatar {
-        flex: 0 auto;
-        width: 48px;
-        height: 48px;
-        font-size: 14px;
-        line-height: 48px;
-      }
-
-      .actions {
-        display: flex;
-        align-items: baseline;
-
-        .checkbox {
-          display: inline-flex;
-          align-items: baseline;
-          margin-right: 1em;
-          flex: 1;
-        }
-      }
-
-      .separator {
-        margin: 1em;
-        border-bottom: 1px solid;
-        border-color: $fallback--border;
-        border-color: var(--border, $fallback--border);
-      }
-
-      .panel-heading {
-        .badge, .alert, .btn, .faint {
-          margin-left: 1em;
-          white-space: nowrap;
-        }
-        .faint {
-          text-overflow: ellipsis;
-          min-width: 2em;
-          overflow-x: hidden;
-        }
-        .flex-spacer {
-          flex: 1;
-        }
-      }
-      .btn {
-        margin-left: 0;
-        padding: 0 1em;
-        min-width: 3em;
-        min-height: 30px;
-      }
-    }
-  }
-
-  .apply-container {
-    justify-content: center;
-  }
-
-  .radius-item,
-  .color-item {
-    min-width: 20em;
-    margin: 5px 6px 0 0;
-    display:flex;
-    flex-direction: column;
-    flex: 1 1 0;
-
-    &.wide {
-      min-width: 60%
-    }
-
-    &:not(.wide):nth-child(2n+1) {
-      margin-right: 7px;
-
-    }
-
-    .color, .opacity {
-      display:flex;
-      align-items: baseline;
-    }
-  }
-
-  .radius-item {
-    flex-basis: auto;
-  }
-
-  .theme-radius-rn,
-  .theme-color-cl {
-    border: 0;
-    box-shadow: none;
-    background: transparent;
-    color: var(--faint, $fallback--faint);
-    align-self: stretch;
-  }
-
-  .theme-color-cl,
-  .theme-radius-in,
-  .theme-color-in {
-    margin-left: 4px;
-  }
-
-  .theme-radius-in {
-    min-width: 1em;
-  }
-
-  .theme-radius-in {
-    max-width: 7em;
-    flex: 1;
-  }
-
-  .theme-radius-lb{
-    max-width: 50em;
-  }
-
-  .theme-preview-content {
-    padding: 20px;
-  }
-
-  .btn {
-    margin-left: .25em;
-    margin-right: .25em;
-  }
-}
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
deleted file mode 100644
index 62c8e634..00000000
--- a/src/components/style_switcher/style_switcher.vue
+++ /dev/null
@@ -1,956 +0,0 @@
-<template>
-  <div class="style-switcher">
-    <div class="presets-container">
-      <div class="save-load">
-        <div
-          v-if="themeWarning"
-          class="theme-warning"
-        >
-          <div class="alert warning">
-            {{ themeWarningHelp }}
-          </div>
-          <div class="buttons">
-            <template v-if="themeWarning.type === 'snapshot_source_mismatch'">
-              <button
-                class="btn"
-                @click="forceLoad"
-              >
-                {{ $t('settings.style.switcher.use_source') }}
-              </button>
-              <button
-                class="btn"
-                @click="forceSnapshot"
-              >
-                {{ $t('settings.style.switcher.use_snapshot') }}
-              </button>
-            </template>
-            <template v-else-if="themeWarning.noActionsPossible">
-              <button
-                class="btn"
-                @click="dismissWarning"
-              >
-                {{ $t('general.dismiss') }}
-              </button>
-            </template>
-            <template v-else>
-              <button
-                class="btn"
-                @click="forceLoad"
-              >
-                {{ $t('settings.style.switcher.load_theme') }}
-              </button>
-              <button
-                class="btn"
-                @click="dismissWarning"
-              >
-                {{ $t('settings.style.switcher.keep_as_is') }}
-              </button>
-            </template>
-          </div>
-        </div>
-        <ExportImport
-          :export-object="exportedTheme"
-          :export-label="$t(&quot;settings.export_theme&quot;)"
-          :import-label="$t(&quot;settings.import_theme&quot;)"
-          :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
-          :on-import="onImport"
-          :validator="importValidator"
-        >
-          <template slot="before">
-            <div class="presets">
-              {{ $t('settings.presets') }}
-              <label
-                for="preset-switcher"
-                class="select"
-              >
-                <select
-                  id="preset-switcher"
-                  v-model="selected"
-                  class="preset-switcher"
-                >
-                  <option
-                    v-for="style in availableStyles"
-                    :key="style.name"
-                    :value="style"
-                    :style="{
-                      backgroundColor: style[1] || (style.theme || style.source).colors.bg,
-                      color: style[3] || (style.theme || style.source).colors.text
-                    }"
-                  >
-                    {{ style[0] || style.name }}
-                  </option>
-                </select>
-                <i class="icon-down-open" />
-              </label>
-            </div>
-          </template>
-        </ExportImport>
-      </div>
-      <div class="save-load-options">
-        <span class="keep-option">
-          <Checkbox v-model="keepColor">
-            {{ $t('settings.style.switcher.keep_color') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepShadows">
-            {{ $t('settings.style.switcher.keep_shadows') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepOpacity">
-            {{ $t('settings.style.switcher.keep_opacity') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepRoundness">
-            {{ $t('settings.style.switcher.keep_roundness') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepFonts">
-            {{ $t('settings.style.switcher.keep_fonts') }}
-          </Checkbox>
-        </span>
-        <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
-      </div>
-    </div>
-
-    <preview :style="previewRules" />
-
-    <keep-alive>
-      <tab-switcher key="style-tweak">
-        <div
-          :label="$t('settings.style.common_colors._tab_label')"
-          class="color-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.theme_help') }}</p>
-            <button
-              class="btn"
-              @click="clearOpacity"
-            >
-              {{ $t('settings.style.switcher.clear_opacity') }}
-            </button>
-            <button
-              class="btn"
-              @click="clearV1"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <p>{{ $t('settings.theme_help_v2_1') }}</p>
-          <h4>{{ $t('settings.style.common_colors.main') }}</h4>
-          <div class="color-item">
-            <ColorInput
-              v-model="bgColorLocal"
-              name="bgColor"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="bgOpacityLocal"
-              name="bgOpacity"
-              :fallback="previewTheme.opacity.bg"
-            />
-            <ColorInput
-              v-model="textColorLocal"
-              name="textColor"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgText" />
-            <ColorInput
-              v-model="accentColorLocal"
-              name="accentColor"
-              :fallback="previewTheme.colors.link"
-              :label="$t('settings.accent')"
-              :show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
-            />
-            <ColorInput
-              v-model="linkColorLocal"
-              name="linkColor"
-              :fallback="previewTheme.colors.accent"
-              :label="$t('settings.links')"
-              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
-            />
-            <ContrastRatio :contrast="previewContrast.bgLink" />
-          </div>
-          <div class="color-item">
-            <ColorInput
-              v-model="fgColorLocal"
-              name="fgColor"
-              :label="$t('settings.foreground')"
-            />
-            <ColorInput
-              v-model="fgTextColorLocal"
-              name="fgTextColor"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.fgText"
-            />
-            <ColorInput
-              v-model="fgLinkColorLocal"
-              name="fgLinkColor"
-              :label="$t('settings.links')"
-              :fallback="previewTheme.colors.fgLink"
-            />
-            <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
-          </div>
-          <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
-          <div class="color-item">
-            <ColorInput
-              v-model="cRedColorLocal"
-              name="cRedColor"
-              :label="$t('settings.cRed')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCRed" />
-            <ColorInput
-              v-model="cBlueColorLocal"
-              name="cBlueColor"
-              :label="$t('settings.cBlue')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCBlue" />
-          </div>
-          <div class="color-item">
-            <ColorInput
-              v-model="cGreenColorLocal"
-              name="cGreenColor"
-              :label="$t('settings.cGreen')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCGreen" />
-            <ColorInput
-              v-model="cOrangeColorLocal"
-              name="cOrangeColor"
-              :label="$t('settings.cOrange')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCOrange" />
-          </div>
-          <p>{{ $t('settings.theme_help_v2_2') }}</p>
-        </div>
-
-        <div
-          :label="$t('settings.style.advanced_colors._tab_label')"
-          class="color-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.theme_help') }}</p>
-            <button
-              class="btn"
-              @click="clearOpacity"
-            >
-              {{ $t('settings.style.switcher.clear_opacity') }}
-            </button>
-            <button
-              class="btn"
-              @click="clearV1"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
-            <ColorInput
-              v-model="postLinkColorLocal"
-              name="postLinkColor"
-              :fallback="previewTheme.colors.accent"
-              :label="$t('settings.links')"
-            />
-            <ContrastRatio :contrast="previewContrast.postLink" />
-            <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
-            <ColorInput
-              v-model="alertErrorColorLocal"
-              name="alertError"
-              :label="$t('settings.style.advanced_colors.alert_error')"
-              :fallback="previewTheme.colors.alertError"
-            />
-            <ColorInput
-              v-model="alertErrorTextColorLocal"
-              name="alertErrorText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.alertErrorText"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.alertErrorText"
-              large="true"
-            />
-            <ColorInput
-              v-model="alertWarningColorLocal"
-              name="alertWarning"
-              :label="$t('settings.style.advanced_colors.alert_warning')"
-              :fallback="previewTheme.colors.alertWarning"
-            />
-            <ColorInput
-              v-model="alertWarningTextColorLocal"
-              name="alertWarningText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.alertWarningText"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.alertWarningText"
-              large="true"
-            />
-            <ColorInput
-              v-model="alertNeutralColorLocal"
-              name="alertNeutral"
-              :label="$t('settings.style.advanced_colors.alert_neutral')"
-              :fallback="previewTheme.colors.alertNeutral"
-            />
-            <ColorInput
-              v-model="alertNeutralTextColorLocal"
-              name="alertNeutralText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.alertNeutralText"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.alertNeutralText"
-              large="true"
-            />
-            <OpacityInput
-              v-model="alertOpacityLocal"
-              name="alertOpacity"
-              :fallback="previewTheme.opacity.alert"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
-            <ColorInput
-              v-model="badgeNotificationColorLocal"
-              name="badgeNotification"
-              :label="$t('settings.style.advanced_colors.badge_notification')"
-              :fallback="previewTheme.colors.badgeNotification"
-            />
-            <ColorInput
-              v-model="badgeNotificationTextColorLocal"
-              name="badgeNotificationText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.badgeNotificationText"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.badgeNotificationText"
-              large="true"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
-            <ColorInput
-              v-model="panelColorLocal"
-              name="panelColor"
-              :fallback="previewTheme.colors.panel"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="panelOpacityLocal"
-              name="panelOpacity"
-              :fallback="previewTheme.opacity.panel"
-              :disabled="panelColorLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="panelTextColorLocal"
-              name="panelTextColor"
-              :fallback="previewTheme.colors.panelText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.panelText"
-              large="true"
-            />
-            <ColorInput
-              v-model="panelLinkColorLocal"
-              name="panelLinkColor"
-              :fallback="previewTheme.colors.panelLink"
-              :label="$t('settings.links')"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.panelLink"
-              large="true"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
-            <ColorInput
-              v-model="topBarColorLocal"
-              name="topBarColor"
-              :fallback="previewTheme.colors.topBar"
-              :label="$t('settings.background')"
-            />
-            <ColorInput
-              v-model="topBarTextColorLocal"
-              name="topBarTextColor"
-              :fallback="previewTheme.colors.topBarText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.topBarText" />
-            <ColorInput
-              v-model="topBarLinkColorLocal"
-              name="topBarLinkColor"
-              :fallback="previewTheme.colors.topBarLink"
-              :label="$t('settings.links')"
-            />
-            <ContrastRatio :contrast="previewContrast.topBarLink" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
-            <ColorInput
-              v-model="inputColorLocal"
-              name="inputColor"
-              :fallback="previewTheme.colors.input"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="inputOpacityLocal"
-              name="inputOpacity"
-              :fallback="previewTheme.opacity.input"
-              :disabled="inputColorLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="inputTextColorLocal"
-              name="inputTextColor"
-              :fallback="previewTheme.colors.inputText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.inputText" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
-            <ColorInput
-              v-model="btnColorLocal"
-              name="btnColor"
-              :fallback="previewTheme.colors.btn"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="btnOpacityLocal"
-              name="btnOpacity"
-              :fallback="previewTheme.opacity.btn"
-              :disabled="btnColorLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="btnTextColorLocal"
-              name="btnTextColor"
-              :fallback="previewTheme.colors.btnText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnText" />
-            <ColorInput
-              v-model="btnPanelTextColorLocal"
-              name="btnPanelTextColor"
-              :fallback="previewTheme.colors.btnPanelText"
-              :label="$t('settings.style.advanced_colors.panel_header')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnPanelText" />
-            <ColorInput
-              v-model="btnTopBarTextColorLocal"
-              name="btnTopBarTextColor"
-              :fallback="previewTheme.colors.btnTopBarText"
-              :label="$t('settings.style.advanced_colors.top_bar')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnTopBarText" />
-            <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
-            <ColorInput
-              v-model="btnPressedColorLocal"
-              name="btnPressedColor"
-              :fallback="previewTheme.colors.btnPressed"
-              :label="$t('settings.background')"
-            />
-            <ColorInput
-              v-model="btnPressedTextColorLocal"
-              name="btnPressedTextColor"
-              :fallback="previewTheme.colors.btnPressedText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnPressedText" />
-            <ColorInput
-              v-model="btnPressedPanelTextColorLocal"
-              name="btnPressedPanelTextColor"
-              :fallback="previewTheme.colors.btnPressedPanelText"
-              :label="$t('settings.style.advanced_colors.panel_header')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
-            <ColorInput
-              v-model="btnPressedTopBarTextColorLocal"
-              name="btnPressedTopBarTextColor"
-              :fallback="previewTheme.colors.btnPressedTopBarText"
-              :label="$t('settings.style.advanced_colors.top_bar')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
-            <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
-            <ColorInput
-              v-model="btnDisabledColorLocal"
-              name="btnDisabledColor"
-              :fallback="previewTheme.colors.btnDisabled"
-              :label="$t('settings.background')"
-            />
-            <ColorInput
-              v-model="btnDisabledTextColorLocal"
-              name="btnDisabledTextColor"
-              :fallback="previewTheme.colors.btnDisabledText"
-              :label="$t('settings.text')"
-            />
-            <ColorInput
-              v-model="btnDisabledPanelTextColorLocal"
-              name="btnDisabledPanelTextColor"
-              :fallback="previewTheme.colors.btnDisabledPanelText"
-              :label="$t('settings.style.advanced_colors.panel_header')"
-            />
-            <ColorInput
-              v-model="btnDisabledTopBarTextColorLocal"
-              name="btnDisabledTopBarTextColor"
-              :fallback="previewTheme.colors.btnDisabledTopBarText"
-              :label="$t('settings.style.advanced_colors.top_bar')"
-            />
-            <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
-            <ColorInput
-              v-model="btnToggledColorLocal"
-              name="btnToggledColor"
-              :fallback="previewTheme.colors.btnToggled"
-              :label="$t('settings.background')"
-            />
-            <ColorInput
-              v-model="btnToggledTextColorLocal"
-              name="btnToggledTextColor"
-              :fallback="previewTheme.colors.btnToggledText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnToggledText" />
-            <ColorInput
-              v-model="btnToggledPanelTextColorLocal"
-              name="btnToggledPanelTextColor"
-              :fallback="previewTheme.colors.btnToggledPanelText"
-              :label="$t('settings.style.advanced_colors.panel_header')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
-            <ColorInput
-              v-model="btnToggledTopBarTextColorLocal"
-              name="btnToggledTopBarTextColor"
-              :fallback="previewTheme.colors.btnToggledTopBarText"
-              :label="$t('settings.style.advanced_colors.top_bar')"
-            />
-            <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
-            <ColorInput
-              v-model="tabColorLocal"
-              name="tabColor"
-              :fallback="previewTheme.colors.tab"
-              :label="$t('settings.background')"
-            />
-            <ColorInput
-              v-model="tabTextColorLocal"
-              name="tabTextColor"
-              :fallback="previewTheme.colors.tabText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.tabText" />
-            <ColorInput
-              v-model="tabActiveTextColorLocal"
-              name="tabActiveTextColor"
-              :fallback="previewTheme.colors.tabActiveText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.tabActiveText" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
-            <ColorInput
-              v-model="borderColorLocal"
-              name="borderColor"
-              :fallback="previewTheme.colors.border"
-              :label="$t('settings.style.common.color')"
-            />
-            <OpacityInput
-              v-model="borderOpacityLocal"
-              name="borderOpacity"
-              :fallback="previewTheme.opacity.border"
-              :disabled="borderColorLocal === 'transparent'"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
-            <ColorInput
-              v-model="faintColorLocal"
-              name="faintColor"
-              :fallback="previewTheme.colors.faint"
-              :label="$t('settings.text')"
-            />
-            <ColorInput
-              v-model="faintLinkColorLocal"
-              name="faintLinkColor"
-              :fallback="previewTheme.colors.faintLink"
-              :label="$t('settings.links')"
-            />
-            <ColorInput
-              v-model="panelFaintColorLocal"
-              name="panelFaintColor"
-              :fallback="previewTheme.colors.panelFaint"
-              :label="$t('settings.style.advanced_colors.panel_header')"
-            />
-            <OpacityInput
-              v-model="faintOpacityLocal"
-              name="faintOpacity"
-              :fallback="previewTheme.opacity.faint"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
-            <ColorInput
-              v-model="underlayColorLocal"
-              name="underlay"
-              :label="$t('settings.style.advanced_colors.underlay')"
-              :fallback="previewTheme.colors.underlay"
-            />
-            <OpacityInput
-              v-model="underlayOpacityLocal"
-              name="underlayOpacity"
-              :fallback="previewTheme.opacity.underlay"
-              :disabled="underlayOpacityLocal === 'transparent'"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
-            <ColorInput
-              v-model="pollColorLocal"
-              name="poll"
-              :label="$t('settings.background')"
-              :fallback="previewTheme.colors.poll"
-            />
-            <ColorInput
-              v-model="pollTextColorLocal"
-              name="pollText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.pollText"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
-            <ColorInput
-              v-model="iconColorLocal"
-              name="icon"
-              :label="$t('settings.style.advanced_colors.icons')"
-              :fallback="previewTheme.colors.icon"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
-            <ColorInput
-              v-model="highlightColorLocal"
-              name="highlight"
-              :label="$t('settings.background')"
-              :fallback="previewTheme.colors.highlight"
-            />
-            <ColorInput
-              v-model="highlightTextColorLocal"
-              name="highlightText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.highlightText"
-            />
-            <ContrastRatio :contrast="previewContrast.highlightText" />
-            <ColorInput
-              v-model="highlightLinkColorLocal"
-              name="highlightLink"
-              :label="$t('settings.links')"
-              :fallback="previewTheme.colors.highlightLink"
-            />
-            <ContrastRatio :contrast="previewContrast.highlightLink" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
-            <ColorInput
-              v-model="popoverColorLocal"
-              name="popover"
-              :label="$t('settings.background')"
-              :fallback="previewTheme.colors.popover"
-            />
-            <OpacityInput
-              v-model="popoverOpacityLocal"
-              name="popoverOpacity"
-              :fallback="previewTheme.opacity.popover"
-              :disabled="popoverOpacityLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="popoverTextColorLocal"
-              name="popoverText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.popoverText"
-            />
-            <ContrastRatio :contrast="previewContrast.popoverText" />
-            <ColorInput
-              v-model="popoverLinkColorLocal"
-              name="popoverLink"
-              :label="$t('settings.links')"
-              :fallback="previewTheme.colors.popoverLink"
-            />
-            <ContrastRatio :contrast="previewContrast.popoverLink" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
-            <ColorInput
-              v-model="selectedPostColorLocal"
-              name="selectedPost"
-              :label="$t('settings.background')"
-              :fallback="previewTheme.colors.selectedPost"
-            />
-            <ColorInput
-              v-model="selectedPostTextColorLocal"
-              name="selectedPostText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.selectedPostText"
-            />
-            <ContrastRatio :contrast="previewContrast.selectedPostText" />
-            <ColorInput
-              v-model="selectedPostLinkColorLocal"
-              name="selectedPostLink"
-              :label="$t('settings.links')"
-              :fallback="previewTheme.colors.selectedPostLink"
-            />
-            <ContrastRatio :contrast="previewContrast.selectedPostLink" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
-            <ColorInput
-              v-model="selectedMenuColorLocal"
-              name="selectedMenu"
-              :label="$t('settings.background')"
-              :fallback="previewTheme.colors.selectedMenu"
-            />
-            <ColorInput
-              v-model="selectedMenuTextColorLocal"
-              name="selectedMenuText"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.selectedMenuText"
-            />
-            <ContrastRatio :contrast="previewContrast.selectedMenuText" />
-            <ColorInput
-              v-model="selectedMenuLinkColorLocal"
-              name="selectedMenuLink"
-              :label="$t('settings.links')"
-              :fallback="previewTheme.colors.selectedMenuLink"
-            />
-            <ContrastRatio :contrast="previewContrast.selectedMenuLink" />
-          </div>
-        </div>
-
-        <div
-          :label="$t('settings.style.radii._tab_label')"
-          class="radius-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.radii_help') }}</p>
-            <button
-              class="btn"
-              @click="clearRoundness"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <RangeInput
-            v-model="btnRadiusLocal"
-            name="btnRadius"
-            :label="$t('settings.btnRadius')"
-            :fallback="previewTheme.radii.btn"
-            max="16"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="inputRadiusLocal"
-            name="inputRadius"
-            :label="$t('settings.inputRadius')"
-            :fallback="previewTheme.radii.input"
-            max="9"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="checkboxRadiusLocal"
-            name="checkboxRadius"
-            :label="$t('settings.checkboxRadius')"
-            :fallback="previewTheme.radii.checkbox"
-            max="16"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="panelRadiusLocal"
-            name="panelRadius"
-            :label="$t('settings.panelRadius')"
-            :fallback="previewTheme.radii.panel"
-            max="50"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="avatarRadiusLocal"
-            name="avatarRadius"
-            :label="$t('settings.avatarRadius')"
-            :fallback="previewTheme.radii.avatar"
-            max="28"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="avatarAltRadiusLocal"
-            name="avatarAltRadius"
-            :label="$t('settings.avatarAltRadius')"
-            :fallback="previewTheme.radii.avatarAlt"
-            max="28"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="attachmentRadiusLocal"
-            name="attachmentRadius"
-            :label="$t('settings.attachmentRadius')"
-            :fallback="previewTheme.radii.attachment"
-            max="50"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="tooltipRadiusLocal"
-            name="tooltipRadius"
-            :label="$t('settings.tooltipRadius')"
-            :fallback="previewTheme.radii.tooltip"
-            max="50"
-            hard-min="0"
-          />
-        </div>
-
-        <div
-          :label="$t('settings.style.shadows._tab_label')"
-          class="shadow-container"
-        >
-          <div class="tab-header shadow-selector">
-            <div class="select-container">
-              {{ $t('settings.style.shadows.component') }}
-              <label
-                for="shadow-switcher"
-                class="select"
-              >
-                <select
-                  id="shadow-switcher"
-                  v-model="shadowSelected"
-                  class="shadow-switcher"
-                >
-                  <option
-                    v-for="shadow in shadowsAvailable"
-                    :key="shadow"
-                    :value="shadow"
-                  >
-                    {{ $t('settings.style.shadows.components.' + shadow) }}
-                  </option>
-                </select>
-                <i class="icon-down-open" />
-              </label>
-            </div>
-            <div class="override">
-              <label
-                for="override"
-                class="label"
-              >
-                {{ $t('settings.style.shadows.override') }}
-              </label>
-              <input
-                id="override"
-                v-model="currentShadowOverriden"
-                name="override"
-                class="input-override"
-                type="checkbox"
-              >
-              <label
-                class="checkbox-label"
-                for="override"
-              />
-            </div>
-            <button
-              class="btn"
-              @click="clearShadows"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <ShadowControl
-            v-model="currentShadow"
-            :ready="!!currentShadowFallback"
-            :fallback="currentShadowFallback"
-          />
-          <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
-            <i18n
-              path="settings.style.shadows.filter_hint.always_drop_shadow"
-              tag="p"
-            >
-              <code>filter: drop-shadow()</code>
-            </i18n>
-            <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
-            <i18n
-              path="settings.style.shadows.filter_hint.drop_shadow_syntax"
-              tag="p"
-            >
-              <code>drop-shadow</code>
-              <code>spread-radius</code>
-              <code>inset</code>
-            </i18n>
-            <i18n
-              path="settings.style.shadows.filter_hint.inset_classic"
-              tag="p"
-            >
-              <code>box-shadow</code>
-            </i18n>
-            <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
-          </div>
-        </div>
-
-        <div
-          :label="$t('settings.style.fonts._tab_label')"
-          class="fonts-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.style.fonts.help') }}</p>
-            <button
-              class="btn"
-              @click="clearFonts"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <FontControl
-            v-model="fontsLocal.interface"
-            name="ui"
-            :label="$t('settings.style.fonts.components.interface')"
-            :fallback="previewTheme.fonts.interface"
-            no-inherit="1"
-          />
-          <FontControl
-            v-model="fontsLocal.input"
-            name="input"
-            :label="$t('settings.style.fonts.components.input')"
-            :fallback="previewTheme.fonts.input"
-          />
-          <FontControl
-            v-model="fontsLocal.post"
-            name="post"
-            :label="$t('settings.style.fonts.components.post')"
-            :fallback="previewTheme.fonts.post"
-          />
-          <FontControl
-            v-model="fontsLocal.postCode"
-            name="postCode"
-            :label="$t('settings.style.fonts.components.postCode')"
-            :fallback="previewTheme.fonts.postCode"
-          />
-        </div>
-      </tab-switcher>
-    </keep-alive>
-
-    <div class="apply-container">
-      <button
-        class="btn submit"
-        :disabled="!themeValid"
-        @click="setCustomTheme"
-      >
-        {{ $t('general.apply') }}
-      </button>
-      <button
-        class="btn"
-        @click="clearAll"
-      >
-        {{ $t('settings.style.switcher.reset') }}
-      </button>
-    </div>
-  </div>
-</template>
-
-<script src="./style_switcher.js"></script>
-
-<style src="./style_switcher.scss" lang="scss"></style>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index a443531e..a7b790a3 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -46,8 +46,14 @@
 
   &.side-tabs {
     flex-direction: row;
+    @media all and (max-width: 800px) {
+      overflow-x: auto;
+    }
     > .contents {
       flex: 0 1 80%;
+      @media all and (max-width: 800px) {
+        min-width: 96vw;
+      }
     }
     > .tabs {
       flex: 1 0 auto;
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 4ee040e8..b4f275d9 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -50,15 +50,6 @@
               >
                 {{ user.name }}
               </div>
-              <router-link
-                v-if="!isOtherUser"
-                :to="{ name: 'user-settings' }"
-              >
-                <i
-                  class="button-icon icon-wrench usersettings"
-                  :title="$t('tool_tip.user_settings')"
-                />
-              </router-link>
               <a
                 v-if="isOtherUser && !user.is_local"
                 :href="user.statusnet_profile_url"
@@ -117,7 +108,7 @@
               type="color"
             >
             <label
-              for="style-switcher"
+              for="theme_tab"
               class="userHighlightSel select"
             >
               <select
diff --git a/src/components/user_settings/confirm.js b/src/components/user_settings/confirm.js
deleted file mode 100644
index 0f4ddfc9..00000000
--- a/src/components/user_settings/confirm.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const Confirm = {
-  props: ['disabled'],
-  data: () => ({}),
-  methods: {
-    confirm () { this.$emit('confirm') },
-    cancel () { this.$emit('cancel') }
-  }
-}
-export default Confirm
diff --git a/src/components/user_settings/confirm.vue b/src/components/user_settings/confirm.vue
deleted file mode 100644
index 69b3811b..00000000
--- a/src/components/user_settings/confirm.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-<template>
-  <div>
-    <slot />
-    <button
-      class="btn btn-default"
-      :disabled="disabled"
-      @click="confirm"
-    >
-      {{ $t('general.confirm') }}
-    </button>
-    <button
-      class="btn btn-default"
-      :disabled="disabled"
-      @click="cancel"
-    >
-      {{ $t('general.cancel') }}
-    </button>
-  </div>
-</template>
-
-<script src="./confirm.js">
-</script>
diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js
deleted file mode 100644
index abf37062..00000000
--- a/src/components/user_settings/mfa.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import RecoveryCodes from './mfa_backup_codes.vue'
-import TOTP from './mfa_totp.vue'
-import Confirm from './confirm.vue'
-import VueQrcode from '@chenfengyuan/vue-qrcode'
-import { mapState } from 'vuex'
-
-const Mfa = {
-  data: () => ({
-    settings: { // current settings of MFA
-      available: false,
-      enabled: false,
-      totp: false
-    },
-    setupState: { // setup mfa
-      state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
-      setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
-    },
-    backupCodes: {
-      getNewCodes: false,
-      inProgress: false, //  progress of fetch codes
-      codes: []
-    },
-    otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
-      provisioning_uri: '',
-      key: ''
-    },
-    currentPassword: null,
-    otpConfirmToken: null,
-    error: null,
-    readyInit: false
-  }),
-  components: {
-    'recovery-codes': RecoveryCodes,
-    'totp-item': TOTP,
-    'qrcode': VueQrcode,
-    'confirm': Confirm
-  },
-  computed: {
-    canSetupOTP () {
-      return (
-        (this.setupInProgress && this.backupCodesPrepared) ||
-          this.settings.enabled
-      ) && !this.settings.totp && !this.setupOTPInProgress
-    },
-    setupInProgress () {
-      return this.setupState.state !== '' && this.setupState.state !== 'complete'
-    },
-    setupOTPInProgress () {
-      return this.setupState.state === 'setupOTP' && !this.completedOTP
-    },
-    prepareOTP () {
-      return this.setupState.setupOTPState === 'prepare'
-    },
-    confirmOTP () {
-      return this.setupState.setupOTPState === 'confirm'
-    },
-    completedOTP () {
-      return this.setupState.setupOTPState === 'completed'
-    },
-    backupCodesPrepared () {
-      return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
-    },
-    confirmNewBackupCodes () {
-      return this.backupCodes.getNewCodes
-    },
-    ...mapState({
-      backendInteractor: (state) => state.api.backendInteractor
-    })
-  },
-
-  methods: {
-    activateOTP () {
-      if (!this.settings.enabled) {
-        this.setupState.state = 'getBackupcodes'
-        this.fetchBackupCodes()
-      }
-    },
-    fetchBackupCodes () {
-      this.backupCodes.inProgress = true
-      this.backupCodes.codes = []
-
-      return this.backendInteractor.generateMfaBackupCodes()
-        .then((res) => {
-          this.backupCodes.codes = res.codes
-          this.backupCodes.inProgress = false
-        })
-    },
-    getBackupCodes () { // get a new backup codes
-      this.backupCodes.getNewCodes = true
-    },
-    confirmBackupCodes () { // confirm getting new backup codes
-      this.fetchBackupCodes().then((res) => {
-        this.backupCodes.getNewCodes = false
-      })
-    },
-    cancelBackupCodes () { // cancel confirm form of new backup codes
-      this.backupCodes.getNewCodes = false
-    },
-
-    // Setup OTP
-    setupOTP () { // prepare setup OTP
-      this.setupState.state = 'setupOTP'
-      this.setupState.setupOTPState = 'prepare'
-      this.backendInteractor.mfaSetupOTP()
-        .then((res) => {
-          this.otpSettings = res
-          this.setupState.setupOTPState = 'confirm'
-        })
-    },
-    doConfirmOTP () { // handler confirm enable OTP
-      this.error = null
-      this.backendInteractor.mfaConfirmOTP({
-        token: this.otpConfirmToken,
-        password: this.currentPassword
-      })
-        .then((res) => {
-          if (res.error) {
-            this.error = res.error
-            return
-          }
-          this.completeSetup()
-        })
-    },
-
-    completeSetup () {
-      this.setupState.setupOTPState = 'complete'
-      this.setupState.state = 'complete'
-      this.currentPassword = null
-      this.error = null
-      this.fetchSettings()
-    },
-    cancelSetup () { // cancel setup
-      this.setupState.setupOTPState = ''
-      this.setupState.state = ''
-      this.currentPassword = null
-      this.error = null
-    },
-    // end Setup OTP
-
-    // fetch settings from server
-    async fetchSettings () {
-      let result = await this.backendInteractor.settingsMFA()
-      if (result.error) return
-      this.settings = result.settings
-      this.settings.available = true
-      return result
-    }
-  },
-  mounted () {
-    this.fetchSettings().then(() => {
-      this.readyInit = true
-    })
-  }
-}
-export default Mfa
diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue
deleted file mode 100644
index 14ea10a1..00000000
--- a/src/components/user_settings/mfa.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<template>
-  <div
-    v-if="readyInit && settings.available"
-    class="setting-item mfa-settings"
-  >
-    <div class="mfa-heading">
-      <h2>{{ $t('settings.mfa.title') }}</h2>
-    </div>
-
-    <div>
-      <div
-        v-if="!setupInProgress"
-        class="setting-item"
-      >
-        <!-- Enabled methods -->
-        <h3>{{ $t('settings.mfa.authentication_methods') }}</h3>
-        <totp-item
-          :settings="settings"
-          @deactivate="fetchSettings"
-          @activate="activateOTP"
-        />
-        <br>
-
-        <div v-if="settings.enabled">
-          <!-- backup codes block-->
-          <recovery-codes
-            v-if="!confirmNewBackupCodes"
-            :backup-codes="backupCodes"
-          />
-          <button
-            v-if="!confirmNewBackupCodes"
-            class="btn btn-default"
-            @click="getBackupCodes"
-          >
-            {{ $t('settings.mfa.generate_new_recovery_codes') }}
-          </button>
-
-          <div v-if="confirmNewBackupCodes">
-            <confirm
-              :disabled="backupCodes.inProgress"
-              @confirm="confirmBackupCodes"
-              @cancel="cancelBackupCodes"
-            >
-              <p class="warning">
-                {{ $t('settings.mfa.warning_of_generate_new_codes') }}
-              </p>
-            </confirm>
-          </div>
-        </div>
-      </div>
-
-      <div v-if="setupInProgress">
-        <!-- setup block-->
-
-        <h3>{{ $t('settings.mfa.setup_otp') }}</h3>
-
-        <recovery-codes
-          v-if="!setupOTPInProgress"
-          :backup-codes="backupCodes"
-        />
-
-        <button
-          v-if="canSetupOTP"
-          class="btn btn-default"
-          @click="cancelSetup"
-        >
-          {{ $t('general.cancel') }}
-        </button>
-
-        <button
-          v-if="canSetupOTP"
-          class="btn btn-default"
-          @click="setupOTP"
-        >
-          {{ $t('settings.mfa.setup_otp') }}
-        </button>
-
-        <template v-if="setupOTPInProgress">
-          <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i>
-
-          <div v-if="confirmOTP">
-            <div class="setup-otp">
-              <div class="qr-code">
-                <h4>{{ $t('settings.mfa.scan.title') }}</h4>
-                <p>{{ $t('settings.mfa.scan.desc') }}</p>
-                <qrcode
-                  :value="otpSettings.provisioning_uri"
-                  :options="{ width: 200 }"
-                />
-                <p>
-                  {{ $t('settings.mfa.scan.secret_code') }}:
-                  {{ otpSettings.key }}
-                </p>
-              </div>
-
-              <div class="verify">
-                <h4>{{ $t('general.verify') }}</h4>
-                <p>{{ $t('settings.mfa.verify.desc') }}</p>
-                <input
-                  v-model="otpConfirmToken"
-                  type="text"
-                >
-
-                <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
-                <input
-                  v-model="currentPassword"
-                  type="password"
-                >
-                <div class="confirm-otp-actions">
-                  <button
-                    class="btn btn-default"
-                    @click="doConfirmOTP"
-                  >
-                    {{ $t('settings.mfa.confirm_and_enable') }}
-                  </button>
-                  <button
-                    class="btn btn-default"
-                    @click="cancelSetup"
-                  >
-                    {{ $t('general.cancel') }}
-                  </button>
-                </div>
-                <div
-                  v-if="error"
-                  class="alert error"
-                >
-                  {{ error }}
-                </div>
-              </div>
-            </div>
-          </div>
-        </template>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script src="./mfa.js"></script>
-<style lang="scss">
-@import '../../_variables.scss';
-.warning {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
-}
-.mfa-settings {
-  .mfa-heading, .method-item {
-    overflow: hidden;
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-    align-items: baseline;
-  }
-
-  .setup-otp {
-    display: flex;
-    justify-content: center;
-    flex-wrap: wrap;
-    .qr-code {
-      flex: 1;
-      padding-right: 10px;
-    }
-    .verify { flex: 1; }
-    .error { margin: 4px 0 0 0; }
-    .confirm-otp-actions {
-      button {
-        width: 15em;
-        margin-top: 5px;
-      }
-
-    }
-  }
-}
-</style>
diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/user_settings/mfa_backup_codes.js
deleted file mode 100644
index f0a984ec..00000000
--- a/src/components/user_settings/mfa_backup_codes.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default {
-  props: {
-    backupCodes: {
-      type: Object,
-      default: () => ({
-        inProgress: false,
-        codes: []
-      })
-    }
-  },
-  data: () => ({}),
-  computed: {
-    inProgress () { return this.backupCodes.inProgress },
-    ready () { return this.backupCodes.codes.length > 0 },
-    displayTitle () { return this.inProgress || this.ready }
-  }
-}
diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue
deleted file mode 100644
index e6c8ede2..00000000
--- a/src/components/user_settings/mfa_backup_codes.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-  <div>
-    <h4 v-if="displayTitle">
-      {{ $t('settings.mfa.recovery_codes') }}
-    </h4>
-    <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i>
-    <template v-if="ready">
-      <p class="alert warning">
-        {{ $t('settings.mfa.recovery_codes_warning') }}
-      </p>
-      <ul class="backup-codes">
-        <li
-          v-for="code in backupCodes.codes"
-          :key="code"
-        >
-          {{ code }}
-        </li>
-      </ul>
-    </template>
-  </div>
-</template>
-<script src="./mfa_backup_codes.js"></script>
-<style lang="scss">
-@import '../../_variables.scss';
-
-.warning {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
-}
-.backup-codes {
-  font-family: var(--postCodeFont, monospace);
-}
-</style>
diff --git a/src/components/user_settings/mfa_totp.js b/src/components/user_settings/mfa_totp.js
deleted file mode 100644
index 8408d8e9..00000000
--- a/src/components/user_settings/mfa_totp.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Confirm from './confirm.vue'
-import { mapState } from 'vuex'
-
-export default {
-  props: ['settings'],
-  data: () => ({
-    error: false,
-    currentPassword: '',
-    deactivate: false,
-    inProgress: false // progress peform request to disable otp method
-  }),
-  components: {
-    'confirm': Confirm
-  },
-  computed: {
-    isActivated () {
-      return this.settings.totp
-    },
-    ...mapState({
-      backendInteractor: (state) => state.api.backendInteractor
-    })
-  },
-  methods: {
-    doActivate () {
-      this.$emit('activate')
-    },
-    cancelDeactivate () { this.deactivate = false },
-    doDeactivate () {
-      this.error = null
-      this.deactivate = true
-    },
-    confirmDeactivate () { // confirm deactivate TOTP method
-      this.error = null
-      this.inProgress = true
-      this.backendInteractor.mfaDisableOTP({
-        password: this.currentPassword
-      })
-        .then((res) => {
-          this.inProgress = false
-          if (res.error) {
-            this.error = res.error
-            return
-          }
-          this.deactivate = false
-          this.$emit('deactivate')
-        })
-    }
-  }
-}
diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/user_settings/mfa_totp.vue
deleted file mode 100644
index c6f2cc7b..00000000
--- a/src/components/user_settings/mfa_totp.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-  <div>
-    <div class="method-item">
-      <strong>{{ $t('settings.mfa.otp') }}</strong>
-      <button
-        v-if="!isActivated"
-        class="btn btn-default"
-        @click="doActivate"
-      >
-        {{ $t('general.enable') }}
-      </button>
-
-      <button
-        v-if="isActivated"
-        class="btn btn-default"
-        :disabled="deactivate"
-        @click="doDeactivate"
-      >
-        {{ $t('general.disable') }}
-      </button>
-    </div>
-
-    <confirm
-      v-if="deactivate"
-      :disabled="inProgress"
-      @confirm="confirmDeactivate"
-      @cancel="cancelDeactivate"
-    >
-      {{ $t('settings.enter_current_password_to_confirm') }}:
-      <input
-        v-model="currentPassword"
-        type="password"
-      >
-    </confirm>
-    <div
-      v-if="error"
-      class="alert error"
-    >
-      {{ error }}
-    </div>
-  </div>
-</template>
-<script src="./mfa_totp.js"></script>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
deleted file mode 100644
index e07d4e56..00000000
--- a/src/components/user_settings/user_settings.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import get from 'lodash/get'
-import map from 'lodash/map'
-import reject from 'lodash/reject'
-import Autosuggest from '../autosuggest/autosuggest.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import BlockCard from '../block_card/block_card.vue'
-import MuteCard from '../mute_card/mute_card.vue'
-import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
-import SelectableList from '../selectable_list/selectable_list.vue'
-import ProgressButton from '../progress_button/progress_button.vue'
-import Importer from '../importer/importer.vue'
-import Exporter from '../exporter/exporter.vue'
-import withSubscription from '../../hocs/with_subscription/with_subscription'
-import Checkbox from '../checkbox/checkbox.vue'
-
-const BlockList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
-  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const MuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const DomainMuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const UserSettings = {
-  data () {
-    return {
-      activeTab: 'profile',
-      newDomainToMute: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    TabSwitcher,
-    BlockList,
-    MuteList,
-    DomainMuteList,
-    BlockCard,
-    MuteCard,
-    DomainMuteCard,
-    ProgressButton,
-    Autosuggest,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    pleromaBackend () {
-      return this.$store.state.instance.pleromaBackend
-    },
-    currentSaveStateNotice () {
-      return this.$store.state.interface.settings.currentSaveStateNotice
-    }
-  },
-  methods: {
-    importFollows (file) {
-      return this.$store.state.api.backendInteractor.importFollows({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    importBlocks (file) {
-      return this.$store.state.api.backendInteractor.importBlocks({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    generateExportableUsersContent (users) {
-      // Get addresses
-      return users.map((user) => {
-        // check is it's a local user
-        if (user && user.is_local) {
-          // append the instance address
-          // eslint-disable-next-line no-undef
-          return user.screen_name + '@' + location.hostname
-        }
-        return user.screen_name
-      }).join('\n')
-    },
-    activateTab (tabName) {
-      this.activeTab = tabName
-    },
-    filterUnblockedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    filterUnMutedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    queryUserIds (query) {
-      return this.$store.dispatch('searchUsers', query)
-        .then((users) => map(users, 'id'))
-    },
-    blockUsers (ids) {
-      return this.$store.dispatch('blockUsers', ids)
-    },
-    unblockUsers (ids) {
-      return this.$store.dispatch('unblockUsers', ids)
-    },
-    muteUsers (ids) {
-      return this.$store.dispatch('muteUsers', ids)
-    },
-    unmuteUsers (ids) {
-      return this.$store.dispatch('unmuteUsers', ids)
-    },
-    unmuteDomains (domains) {
-      return this.$store.dispatch('unmuteDomains', domains)
-    },
-    muteDomain () {
-      return this.$store.dispatch('muteDomain', this.newDomainToMute)
-        .then(() => { this.newDomainToMute = '' })
-    },
-    identity (value) {
-      return value
-    }
-  }
-}
-
-export default UserSettings
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
deleted file mode 100644
index 2a88714f..00000000
--- a/src/components/user_settings/user_settings.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<template>
-  <div class="settings panel panel-default">
-    <div class="panel-heading">
-      <div class="title">
-        {{ $t('settings.user_settings') }}
-      </div>
-      <transition name="fade">
-        <template v-if="currentSaveStateNotice">
-          <div
-            v-if="currentSaveStateNotice.error"
-            class="alert error"
-            @click.prevent
-          >
-            {{ $t('settings.saving_err') }}
-          </div>
-
-          <div
-            v-if="!currentSaveStateNotice.error"
-            class="alert transparent"
-            @click.prevent
-          >
-            {{ $t('settings.saving_ok') }}
-          </div>
-        </template>
-      </transition>
-    </div>
-    <div class="panel-body profile-edit">
-    </div>
-  </div>
-</template>
-
-<script src="./user_settings.js">
-</script>
-
-<style lang="scss">
-@import '../../_variables.scss';
-
-.profile-edit {
-  .bio {
-    margin: 0;
-  }
-
-  .visibility-tray {
-    padding-top: 5px;
-  }
-
-  input[type=file] {
-    padding: 5px;
-    height: auto;
-  }
-
-  .banner {
-    max-width: 100%;
-  }
-
-  .uploading {
-    font-size: 1.5em;
-    margin: 0.25em;
-  }
-
-  .name-changer {
-    width: 100%;
-  }
-
-  .bg {
-    max-width: 100%;
-  }
-
-  .current-avatar {
-    display: block;
-    width: 150px;
-    height: 150px;
-    border-radius: $fallback--avatarRadius;
-    border-radius: var(--avatarRadius, $fallback--avatarRadius);
-  }
-
-  .oauth-tokens {
-    width: 100%;
-
-    th {
-      text-align: left;
-    }
-
-    .actions {
-      text-align: right;
-    }
-  }
-
-  &-usersearch-wrapper {
-    padding: 1em;
-  }
-
-  &-bulk-actions {
-    text-align: right;
-    padding: 0 1em;
-    min-height: 28px;
-
-    button {
-      width: 10em;
-    }
-  }
-
-  &-domain-mute-form {
-    padding: 1em;
-    display: flex;
-    flex-direction: column;
-
-    button {
-      align-self: flex-end;
-      margin-top: 1em;
-      width: 10em;
-    }
-  }
-
-  .setting-subitem {
-    margin-left: 1.75em;
-  }
-}
-</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 312f7283..d42be00f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -68,7 +68,9 @@
     "disable": "Disable",
     "enable": "Enable",
     "confirm": "Confirm",
-    "verify": "Verify"
+    "verify": "Verify",
+    "close": "Close",
+    "peek": "Peek"
   },
   "image_cropper": {
     "crop_picture": "Crop picture",
-- 
cgit v1.2.3-70-g09d2

