diff --git a/package.json b/package.json
index 542086b4..4d68cc6e 100644
--- a/package.json
+++ b/package.json
@@ -29,11 +29,11 @@
     "portal-vue": "^2.1.4",
     "sanitize-html": "^1.13.0",
     "v-click-outside": "^2.1.1",
-    "vue": "^2.5.13",
+    "vue": "^2.6.11",
     "vue-chat-scroll": "^1.2.1",
     "vue-i18n": "^7.3.2",
     "vue-router": "^3.0.1",
-    "vue-template-compiler": "^2.3.4",
+    "vue-template-compiler": "^2.6.11",
     "vuelidate": "^0.7.4",
     "vuex": "^3.0.1",
     "whatwg-fetch": "^2.0.3"
diff --git a/src/App.js b/src/App.js
index bbb41409..6445335a 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
   },
@@ -117,6 +119,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..f2972eda 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -566,7 +566,7 @@ main-router {
     min-height: 0;
     box-sizing: border-box;
     margin: 0;
-    margin-left: .25em;
+    margin-left: .5em;
     min-width: 1px;
     align-self: stretch;
   }
@@ -860,51 +860,6 @@ nav {
   }
 }
 
-.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;
-  }
-}
 .select-multiple {
   display: flex;
   .option-list {
diff --git a/src/App.vue b/src/App.vue
index 7018a5a4..7b9ad3dc 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -46,15 +46,16 @@
             @toggled="onSearchBarToggled"
             @click.stop.native
           />
-          <router-link
+          <a
+            href="#"
             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"
@@ -125,6 +126,7 @@
     <MobilePostStatusButton />
     <UserReportingModal />
     <PostStatusModal />
+    <SettingsModal />
     <portal-target name="modal" />
   </div>
 </template>
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 7400a682..d98a3b50 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -7,10 +7,8 @@ import Interactions from 'components/interactions/interactions.vue'
 import DMs from 'components/dm_timeline/dm_timeline.vue'
 import UserProfile from 'components/user_profile/user_profile.vue'
 import Search from 'components/search/search.vue'
-import Settings from 'components/settings/settings.vue'
 import Registration from 'components/registration/registration.vue'
 import PasswordReset from 'components/password_reset/password_reset.vue'
-import UserSettings from 'components/user_settings/user_settings.vue'
 import FollowRequests from 'components/follow_requests/follow_requests.vue'
 import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
 import Notifications from 'components/notifications/notifications.vue'
@@ -56,12 +54,10 @@ export default (store) => {
     { name: 'external-user-profile', path: '/users/:id', component: UserProfile },
     { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
     { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
-    { name: 'settings', path: '/settings', component: Settings },
     { name: 'registration', path: '/registration', component: Registration },
     { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
     { name: 'registration-token', path: '/registration/:token', component: Registration },
     { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
-    { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
     { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
     { name: 'login', path: '/login', component: AuthForm },
     { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
diff --git a/src/components/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue
new file mode 100644
index 00000000..b68b98f9
--- /dev/null
+++ b/src/components/async_component_error/async_component_error.vue
@@ -0,0 +1,41 @@
+<template>
+  <div class="async-component-error">
+    <div>
+      <h4>
+        {{ $t('general.generic_error') }}
+      </h4>
+      <p>
+        {{ $t('general.error_retry') }}
+      </p>
+      <button
+        class="btn"
+        @click="retry"
+      >
+        {{ $t('general.retry') }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  methods: {
+    retry () {
+      this.$emit('resetAsyncComponent')
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.async-component-error {
+  display: flex;
+  height: 100%;
+  align-items: center;
+  justify-content: center;
+  .btn {
+    margin: .5em;
+    padding: .5em 2em;
+  }
+}
+</style>
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index cee24241..2b58913f 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -1,8 +1,9 @@
 <template>
   <div
     v-show="isOpen"
-    v-body-scroll-lock="isOpen"
+    v-body-scroll-lock="isOpen && !noBackground"
     class="modal-view"
+    :class="classes"
     @click.self="$emit('backdropClicked')"
   >
     <slot />
@@ -15,6 +16,18 @@ export default {
     isOpen: {
       type: Boolean,
       default: true
+    },
+    noBackground: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    classes () {
+      return {
+        'modal-background': !this.noBackground,
+        'open': this.isOpen
+      }
     }
   }
 }
@@ -32,12 +45,22 @@ 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;
+  opacity: 0;
 
-  body:not(.scroll-locked) & {
-    opacity: 0;
+  > * {
+    pointer-events: initial;
+  }
+
+  &.modal-background {
+    pointer-events: initial;
+    background-color: rgba(0, 0, 0, 0.5);
+  }
+
+  &.open {
+    opacity: 1;
   }
 }
 
diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue
new file mode 100644
index 00000000..4efebb3c
--- /dev/null
+++ b/src/components/panel_loading/panel_loading.vue
@@ -0,0 +1,29 @@
+<template>
+  <div class="panel-loading">
+    <span class="loading-text">
+      <i class="icon-spin4 animate-spin" />
+      {{ $t('general.loading') }}
+    </span>
+  </div>
+</template>
+
+<style lang="scss">
+@import 'src/_variables.scss';
+
+.panel-loading {
+  display: flex;
+  height: 100%;
+  align-items: center;
+  justify-content: center;
+  font-size: 2em;
+  color: $fallback--text;
+  color: var(--text, $fallback--text);
+  .loading-text i {
+    font-size: 3em;
+    line-height: 0;
+    vertical-align: middle;
+    color: $fallback--text;
+    color: var(--text, $fallback--text);
+  }
+}
+</style>
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/settings.js b/src/components/settings/settings.js
deleted file mode 100644
index 527b9a8d..00000000
--- a/src/components/settings/settings.js
+++ /dev/null
@@ -1,132 +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,
-      muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
-    }
-  },
-  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.muteWordsStringLocal
-      },
-      set (value) {
-        this.muteWordsStringLocal = 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/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
new file mode 100644
index 00000000..86703697
--- /dev/null
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -0,0 +1,58 @@
+import {
+  instanceDefaultProperties,
+  multiChoiceProperties,
+  defaultState as configDefaultState
+} from 'src/modules/config.js'
+
+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)
+  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/settings_modal.js b/src/components/settings_modal/settings_modal.js
new file mode 100644
index 00000000..f0d49c91
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.js
@@ -0,0 +1,42 @@
+import Modal from 'src/components/modal/modal.vue'
+import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
+import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
+import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
+
+const SettingsModal = {
+  components: {
+    Modal,
+    SettingsModalContent: getResettableAsyncComponent(
+      () => import('./settings_modal_content.vue'),
+      {
+        loading: PanelLoading,
+        error: AsyncComponentError,
+        delay: 0
+      }
+    )
+  },
+  methods: {
+    closeModal () {
+      this.$store.dispatch('closeSettingsModal')
+    },
+    peekModal () {
+      this.$store.dispatch('togglePeekSettingsModal')
+    }
+  },
+  computed: {
+    currentSaveStateNotice () {
+      return this.$store.state.interface.settings.currentSaveStateNotice
+    },
+    modalActivated () {
+      return this.$store.state.interface.settingsModalState !== 'hidden'
+    },
+    modalOpenedOnce () {
+      return this.$store.state.interface.settingsModalLoaded
+    },
+    modalPeeked () {
+      return this.$store.state.interface.settingsModalState === 'minimized'
+    }
+  }
+}
+
+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..833ff89a
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.scss
@@ -0,0 +1,44 @@
+@import 'src/_variables.scss';
+.settings-modal {
+  overflow: hidden;
+
+  &.peek {
+    .settings-modal-panel {
+      /* Explanation:
+       * Modal is positioned vertically centered.
+       * 100vh - 100% = Distance between modal's top+bottom boundaries and screen
+       * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
+       * + 100% - we move modal completely off-screen, it's top boundary touches
+       *   bottom of the screen
+       * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
+       */
+      transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
+    }
+  }
+
+  .settings-modal-panel {
+    overflow: hidden;
+    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;
+
+      .btn {
+        min-height: 28px;
+        min-width: 10em;
+        padding: 0 2em;
+      }
+    }
+  }
+}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
new file mode 100644
index 00000000..6bc64ed0
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.vue
@@ -0,0 +1,54 @@
+<template>
+  <Modal
+    :is-open="modalActivated"
+    class="settings-modal"
+    :class="{ peek: modalPeeked }"
+    :no-background="modalPeeked"
+  >
+    <div class="settings-modal-panel panel">
+      <div class="panel-heading">
+        <span class="title">
+          {{ $t('settings.settings') }}
+        </span>
+        <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>
+        <button
+          class="btn"
+          @click="peekModal"
+        >
+          {{ $t('general.peek') }}
+        </button>
+        <button
+          class="btn"
+          @click="closeModal"
+        >
+          {{ $t('general.close') }}
+        </button>
+      </div>
+      <div class="panel-body">
+        <SettingsModalContent v-if="modalOpenedOnce" />
+      </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/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js
new file mode 100644
index 00000000..48101a90
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_content.js
@@ -0,0 +1,34 @@
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+
+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 SettingsModalContent = {
+  components: {
+    TabSwitcher,
+
+    DataImportExportTab,
+    MutesAndBlocksTab,
+    NotificationsTab,
+    FilteringTab,
+    SecurityTab,
+    ProfileTab,
+    GeneralTab,
+    VersionTab,
+    ThemeTab
+  },
+  computed: {
+    isLoggedIn () {
+      return !!this.$store.state.users.currentUser
+    }
+  }
+}
+
+export default SettingsModalContent
diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss
new file mode 100644
index 00000000..a3fef1cf
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_content.scss
@@ -0,0 +1,43 @@
+@import 'src/_variables.scss';
+.settings_tab-switcher {
+  height: 100%;
+
+  .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;
+    }
+
+    .number-input {
+      max-width: 6em;
+    }
+  }
+}
diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue
new file mode 100644
index 00000000..2156844f
--- /dev/null
+++ b/src/components/settings_modal/settings_modal_content.vue
@@ -0,0 +1,73 @@
+<template>
+  <tab-switcher
+    ref="tabSwitcher"
+    class="settings_tab-switcher"
+    :side-tab-bar="true"
+    :scrollable-tabs="true"
+  >
+    <div
+      :label="$t('settings.general')"
+      icon="wrench"
+    >
+      <GeneralTab />
+    </div>
+    <div
+      v-if="isLoggedIn"
+      :label="$t('settings.profile_tab')"
+      icon="user"
+    >
+      <ProfileTab />
+    </div>
+    <div
+      v-if="isLoggedIn"
+      :label="$t('settings.security_tab')"
+      icon="lock"
+    >
+      <SecurityTab />
+    </div>
+    <div
+      :label="$t('settings.filtering')"
+      icon="filter"
+    >
+      <FilteringTab />
+    </div>
+    <div
+      :label="$t('settings.theme')"
+      icon="brush"
+    >
+      <ThemeTab />
+    </div>
+    <div
+      v-if="isLoggedIn"
+      :label="$t('settings.notifications')"
+      icon="bell-ringing-o"
+    >
+      <NotificationsTab />
+    </div>
+    <div
+      v-if="isLoggedIn"
+      :label="$t('settings.data_import_export_tab')"
+      icon="download"
+    >
+      <DataImportExportTab />
+    </div>
+    <div
+      v-if="isLoggedIn"
+      :label="$t('settings.mutes_and_blocks')"
+      :fullHeight="true"
+      icon="eye-off"
+    >
+      <MutesAndBlocksTab />
+    </div>
+    <div
+      :label="$t('settings.version.title')"
+      icon="info-circled"
+    >
+      <VersionTab />
+    </div>
+  </tab-switcher>
+</template>
+
+<script src="./settings_modal_content.js"></script>
+
+<style src="./settings_modal_content.scss" lang="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..b5d0f5ed
--- /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..224a7f47
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -0,0 +1,44 @@
+import { filter, trim } from 'lodash'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+
+const FilteringTab = {
+  data () {
+    return {
+      muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
+    }
+  },
+  components: {
+    Checkbox
+  },
+  computed: {
+    ...SharedComputedObject(),
+    muteWordsString: {
+      get () {
+        return this.muteWordsStringLocal
+      },
+      set (value) {
+        this.muteWordsStringLocal = value
+        this.$store.dispatch('setOption', {
+          name: 'muteWords',
+          value: filter(value.split('\n'), (word) => trim(word).length > 0)
+        })
+      }
+    }
+  },
+  // 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..eea41514
--- /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..0eb37e44
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -0,0 +1,31 @@
+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 () {
+    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..f89c0480
--- /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/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
new file mode 100644
index 00000000..b0043dbb
--- /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 relationship = this.$store.getters.relationship(this.userId)
+        return relationship.blocking || userId === this.$store.state.users.currentUser.id
+      })
+    },
+    filterUnMutedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const relationship = this.$store.getters.relationship(this.userId)
+        return relationship.muting || userId === 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.scss b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
new file mode 100644
index 00000000..ceb64efb
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
@@ -0,0 +1,29 @@
+.mutes-and-blocks-tab {
+    height: 100%;
+
+    .usersearch-wrapper {
+        padding: 1em;
+    }
+
+    .bulk-actions {
+        text-align: right;
+        padding: 0 1em;
+        min-height: 28px;
+    }
+
+    .bulk-action-button {
+        width: 10em
+    }
+
+    .domain-mute-form {
+        padding: 1em;
+        display: flex;
+        flex-direction: column
+    }
+
+    .domain-mute-button {
+        align-self: flex-end;
+        margin-top: 1em;
+        width: 10em
+    }
+}
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..6884b7be
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -0,0 +1,176 @@
+<template>
+  <tab-switcher
+    :scrollable-tabs="true"
+    class="mutes-and-blocks-tab"
+  >
+    <div :label="$t('settings.blocks_tab')">
+      <div class="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="bulk-actions">
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default bulk-action-button"
+              :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="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="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="domain-mute-form">
+            <input
+              v-model="newDomainToMute"
+              :placeholder="$t('settings.type_domains_to_mute')"
+              type="text"
+              @keyup.enter="muteDomain"
+            >
+            <ProgressButton
+              class="btn btn-default domain-mute-button"
+              :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="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="./mutes_and_blocks_tab.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..b7a3cb37
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -0,0 +1,54 @@
+<template>
+  <div :label="$t('settings.notifications')">
+    <div class="setting-item">
+      <h2>{{ $t('settings.notification_setting_filters') }}</h2>
+      <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>
+    </div>
+
+    <div class="setting-item">
+      <h2>{{ $t('settings.notification_setting_privacy') }}</h2>
+      <p>
+        <Checkbox v-model="notificationSettings.privacy_option">
+          {{ $t('settings.notification_setting_privacy_option') }}
+        </Checkbox>
+      </p>
+    </div>
+    <div class="setting-item">
+      <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_tab.js b/src/components/settings_modal/tabs/profile_tab.js
new file mode 100644
index 00000000..8658b097
--- /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: (query) => this.$store.dispatch('searchUsers', { query })
+      })
+    },
+    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..fff4f970
--- /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/user_settings/confirm.js b/src/components/settings_modal/tabs/security_tab/confirm.js
similarity index 100%
rename from src/components/user_settings/confirm.js
rename to src/components/settings_modal/tabs/security_tab/confirm.js
diff --git a/src/components/user_settings/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue
similarity index 100%
rename from src/components/user_settings/confirm.vue
rename to src/components/settings_modal/tabs/security_tab/confirm.vue
diff --git a/src/components/user_settings/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js
similarity index 100%
rename from src/components/user_settings/mfa.js
rename to src/components/settings_modal/tabs/security_tab/mfa.js
diff --git a/src/components/user_settings/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue
similarity index 97%
rename from src/components/user_settings/mfa.vue
rename to src/components/settings_modal/tabs/security_tab/mfa.vue
index 14ea10a1..25c4d1dc 100644
--- a/src/components/user_settings/mfa.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa.vue
@@ -137,11 +137,7 @@
 
 <script src="./mfa.js"></script>
 <style lang="scss">
-@import '../../_variables.scss';
-.warning {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
-}
+@import '../../../../_variables.scss';
 .mfa-settings {
   .mfa-heading, .method-item {
     overflow: hidden;
@@ -151,6 +147,11 @@
     align-items: baseline;
   }
 
+  .warning {
+    color: $fallback--cOrange;
+    color: var(--cOrange, $fallback--cOrange);
+  }
+
   .setup-otp {
     display: flex;
     justify-content: center;
diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
similarity index 100%
rename from src/components/user_settings/mfa_backup_codes.js
rename to src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
similarity index 69%
rename from src/components/user_settings/mfa_backup_codes.vue
rename to src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
index e6c8ede2..d7e98b3c 100644
--- a/src/components/user_settings/mfa_backup_codes.vue
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="mfa-backup-codes">
     <h4 v-if="displayTitle">
       {{ $t('settings.mfa.recovery_codes') }}
     </h4>
@@ -21,13 +21,15 @@
 </template>
 <script src="./mfa_backup_codes.js"></script>
 <style lang="scss">
-@import '../../_variables.scss';
+@import '../../../../_variables.scss';
 
-.warning {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
-}
-.backup-codes {
-  font-family: var(--postCodeFont, monospace);
+.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/user_settings/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
similarity index 100%
rename from src/components/user_settings/mfa_totp.js
rename to src/components/settings_modal/tabs/security_tab/mfa_totp.js
diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
similarity index 100%
rename from src/components/user_settings/mfa_totp.vue
rename to src/components/settings_modal/tabs/security_tab/mfa_totp.vue
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..3d32d73d
--- /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/style_switcher/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
similarity index 100%
rename from src/components/style_switcher/preview.vue
rename to src/components/settings_modal/tabs/theme_tab/preview.vue
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
similarity index 96%
rename from src/components/style_switcher/style_switcher.js
rename to src/components/settings_modal/tabs/theme_tab/theme_tab.js
index a7f586f4..9d61b0c4 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -3,7 +3,7 @@ import {
   rgb2hex,
   hex2rgb,
   getContrastRatioLayers
-} from '../../services/color_convert/color_convert.js'
+} from 'src/services/color_convert/color_convert.js'
 import {
   DEFAULT_SHADOWS,
   generateColors,
@@ -14,26 +14,27 @@ import {
   getThemes,
   shadows2to3,
   colors2to3
-} from '../../services/style_setter/style_setter.js'
+} from 'src/services/style_setter/style_setter.js'
 import {
   SLOT_INHERITANCE
-} from '../../services/theme_data/pleromafe.js'
+} from 'src/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'
+} 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'
-import ExportImport from '../export_import/export_import.vue'
-import Checkbox from '../checkbox/checkbox.vue'
 
 // List of color values used in v1
 const v1OnlyNames = [
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
similarity index 94%
rename from src/components/style_switcher/style_switcher.scss
rename to src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index d2a40d13..926eceff 100644
--- a/src/components/style_switcher/style_switcher.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -1,5 +1,6 @@
-@import '../../_variables.scss';
-.style-switcher {
+@import 'src/_variables.scss';
+.theme-tab {
+  padding-bottom: 2em;
   .theme-warning {
     display: flex;
     align-items: baseline;
@@ -54,10 +55,6 @@
     }
   }
 
-  .tab-switcher {
-    margin: 0 -1em;
-  }
-
   .reset-container {
     flex-wrap: wrap;
   }
@@ -98,20 +95,25 @@
     align-items: baseline;
     width: 100%;
     min-height: 30px;
-
-    .btn {
-      min-width: 1px;
-      flex: 0 auto;
-      padding: 0 1em;
-    }
+    margin-bottom: 1em;
 
     p {
       flex: 1;
       margin: 0;
       margin-right: .5em;
     }
+  }
 
-    margin-bottom: 1em;
+  .tab-header-buttons {
+    display: flex;
+    flex-direction: column;
+
+    .btn {
+      min-width: 1px;
+      flex: 0 auto;
+      padding: 0 1em;
+      margin-bottom: .5em;
+    }
   }
 
   .shadow-selector {
@@ -161,7 +163,7 @@
     border-bottom: 1px dashed;
     border-color: $fallback--border;
     border-color: var(--border, $fallback--border);
-    margin: 1em -1em 0;
+    margin: 1em 0;
     padding: 1em;
     background: var(--body-background-image);
     background-size: cover;
@@ -328,6 +330,14 @@
     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/style_switcher/style_switcher.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
similarity index 98%
rename from src/components/style_switcher/style_switcher.vue
rename to src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index 62c8e634..fcfad23b 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="style-switcher">
+  <div class="theme-tab">
     <div class="presets-container">
       <div class="save-load">
         <div
@@ -126,18 +126,20 @@
         >
           <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 class="tab-header-buttons">
+              <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>
           <p>{{ $t('settings.theme_help_v2_1') }}</p>
           <h4>{{ $t('settings.style.common_colors.main') }}</h4>
@@ -951,6 +953,6 @@
   </div>
 </template>
 
-<script src="./style_switcher.js"></script>
+<script src="./theme_tab.js"></script>
 
-<style src="./style_switcher.scss" lang="scss"></style>
+<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..d35ff25e
--- /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/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 2181ecc7..d1f044f6 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -62,6 +62,9 @@ const SideDrawer = {
     },
     touchMove (e) {
       GestureService.updateSwipe(e, this.closeGesture)
+    },
+    openSettingsModal () {
+      this.$store.dispatch('openSettingsModal')
     }
   }
 }
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 2958a386..f253742d 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -122,9 +122,12 @@
           </router-link>
         </li>
         <li @click="toggleDrawer">
-          <router-link :to="{ name: 'settings' }">
+          <a
+            href="#"
+            @click="openSettingsModal"
+          >
             <i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
-          </router-link>
+          </a>
         </li>
         <li @click="toggleDrawer">
           <router-link :to="{ name: 'about'}">
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 008e1e95..7891cb78 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 () {
@@ -55,6 +60,9 @@ export default Vue.component('tab-switcher', {
           this.onSwitch.call(null, this.$slots.default[index].key)
         }
         this.active = index
+        if (this.scrollableTabs) {
+          this.$refs.contents.scrollTop = 0
+        }
       }
     }
   },
@@ -64,7 +72,6 @@ export default Vue.component('tab-switcher', {
         if (!slot.tag) return
         const classesTab = ['tab']
         const classesWrapper = ['tab-wrapper']
-
         if (this.activeIndex === index) {
           classesTab.push('active')
           classesWrapper.push('active')
@@ -87,8 +94,14 @@ export default Vue.component('tab-switcher', {
             <button
               disabled={slot.data.attrs.disabled}
               onClick={this.activateTab(index)}
-              class={classesTab.join(' ')}>
-              {slot.data.attrs.label}</button>
+              class={classesTab.join(' ')}
+              type="button"
+            >
+              {!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)}
+              <span class="text">
+                {slot.data.attrs.label}
+              </span>
+            </button>
           </div>
         )
       })
@@ -96,20 +109,32 @@ export default Vue.component('tab-switcher', {
     const contents = this.$slots.default.map((slot, index) => {
       if (!slot.tag) return
       const active = this.activeIndex === index
-      if (this.renderOnlyFocused) {
-        return active
-          ? <div class="active">{slot}</div>
-          : <div class="hidden"></div>
+      const classes = [ active ? 'active' : 'hidden' ]
+      if (slot.data.attrs.fullHeight) {
+        classes.push('full-height')
       }
-      return <div class={active ? 'active' : 'hidden' }>{slot}</div>
+      const renderSlot = (!this.renderOnlyFocused || active)
+        ? slot
+        : ''
+
+      return (
+        <div class={classes}>
+          {
+            this.sideTabBar
+              ? <h1 class="mobile-label">{slot.data.attrs.label}</h1>
+              : ''
+          }
+          {renderSlot}
+        </div>
+      )
     })
 
     return (
-      <div class="tab-switcher">
+      <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
         <div class="tabs">
           {tabs}
         </div>
-        <div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
+        <div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
           {contents}
         </div>
       </div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index df585faa..d2ef4857 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -2,7 +2,144 @@
 
 .tab-switcher {
   display: flex;
-  flex-direction: column;
+
+  .tab-icon {
+    font-size: 2em;
+    display: block;
+  }
+
+  &.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;
+
+    @media all and (max-width: 800px) {
+      overflow-x: auto;
+    }
+
+    > .contents {
+      flex: 1 1 auto;
+    }
+
+    > .tabs {
+      flex: 0 0 auto;
+      overflow-y: auto;
+      overflow-x: hidden;
+      flex-direction: column;
+
+      &::after, &::before {
+        flex-shrink: 0;
+        flex-basis: .5em;
+        content: '';
+        border-right: 1px solid;
+        border-right-color: $fallback--border;
+        border-right-color: var(--border, $fallback--border);
+      }
+
+      &::after {
+        flex-grow: 1;
+      }
+
+      &::before {
+        flex-grow: 0;
+      }
+
+      .tab-wrapper {
+        min-width: 10em;
+        display: flex;
+        flex-direction: column;
+
+        @media all and (max-width: 800px) {
+          min-width: 1em;
+        }
+
+        &: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);
+        }
+
+        &::before {
+          flex: 0 0 6px;
+          content: '';
+          border-right: 1px solid;
+          border-right-color: $fallback--border;
+          border-right-color: var(--border, $fallback--border);
+        }
+
+        &:last-child .tab {
+          margin-bottom: 0;
+        }
+      }
+
+      .tab {
+        flex: 1;
+        box-sizing: content-box;
+        min-width: 10em;
+        min-width: 1px;
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;
+        padding-left: 1em;
+        padding-right: calc(1em + 200px);
+        margin-right: -200px;
+        margin-left: 1em;
+
+        @media all and (max-width: 800px) {
+          padding-left: .25em;
+          padding-right: calc(.25em + 200px);
+          margin-right: calc(.25em - 200px);
+          margin-left: .25em;
+          .text {
+            display: none
+          }
+        }
+      }
+    }
+  }
 
   .contents {
     flex: 1 0 auto;
@@ -11,88 +148,89 @@
     .hidden {
       display: none;
     }
+    .full-height:not(.hidden) {
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      > *:not(.mobile-label) {
+        flex: 1;
+      }
+    }
 
     &.scrollable-tabs {
-      flex-basis: 0;
       overflow-y: auto;
     }
   }
+
+  .tab {
+    position: relative;
+    white-space: nowrap;
+    padding: 6px 1em;
+    background-color: $fallback--fg;
+    background-color: var(--tab, $fallback--fg);
+
+    &, &:active .tab-icon {
+      color: $fallback--text;
+      color: var(--tabText, $fallback--text);
+    }
+
+    &: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-wrapper {
+    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;
-        }
-      }
-
-      &: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;
       }
     }
+  }
 
+  .mobile-label {
+    padding-left: .3em;
+    padding-bottom: .25em;
+    margin-top: .5em;
+    margin-left: .2em;
+    margin-bottom: .25em;
+    border-bottom: 1px solid var(--border, $fallback--border);
+
+    @media all and (min-width: 800px) {
+      display: none;
+    }
   }
 }
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index b40435cd..c4a5ce9d 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"
@@ -118,7 +109,7 @@
               type="color"
             >
             <label
-              for="style-switcher"
+              for="theme_tab"
               class="userHighlightSel select"
             >
               <select
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 9558a0bd..95760bf8 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -3,6 +3,7 @@ import UserCard from '../user_card/user_card.vue'
 import FollowCard from '../follow_card/follow_card.vue'
 import Timeline from '../timeline/timeline.vue'
 import Conversation from '../conversation/conversation.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
 import List from '../list/list.vue'
 import withLoadMore from '../../hocs/with_load_more/with_load_more'
 
@@ -146,6 +147,7 @@ const UserProfile = {
     FollowerList,
     FriendList,
     FollowCard,
+    TabSwitcher,
     Conversation
   }
 }
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
deleted file mode 100644
index a1ec2997..00000000
--- a/src/components/user_settings/user_settings.js
+++ /dev/null
@@ -1,393 +0,0 @@
-import unescape from 'lodash/unescape'
-import get from 'lodash/get'
-import map from 'lodash/map'
-import reject from 'lodash/reject'
-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'),
-  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 {
-      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: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    StyleSwitcher,
-    ScopeSelector,
-    TabSwitcher,
-    ImageCropper,
-    BlockList,
-    MuteList,
-    DomainMuteList,
-    EmojiInput,
-    Autosuggest,
-    BlockCard,
-    MuteCard,
-    DomainMuteCard,
-    ProgressButton,
-    Importer,
-    Exporter,
-    Mfa,
-    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: (query) => this.$store.dispatch('searchUsers', { query })
-      })
-    },
-    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) => {
-          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')
-    },
-    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 relationship = this.$store.getters.relationship(this.userId)
-        return relationship.blocking || userId === this.$store.state.users.currentUser.id
-      })
-    },
-    filterUnMutedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const relationship = this.$store.getters.relationship(this.userId)
-        return relationship.muting || userId === 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 ad184520..00000000
--- a/src/components/user_settings/user_settings.vue
+++ /dev/null
@@ -1,728 +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">
-      <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">
-            <h2>{{ $t('settings.notification_setting_filters') }}</h2>
-            <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>
-          </div>
-
-          <div class="setting-item">
-            <h2>{{ $t('settings.notification_setting_privacy') }}</h2>
-            <p>
-              <Checkbox v-model="notificationSettings.privacy_option">
-                {{ $t('settings.notification_setting_privacy_option') }}
-              </Checkbox>
-            </p>
-          </div>
-          <div class="setting-item">
-            <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>
-
-<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 5bcf074b..fddead69 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -59,7 +59,10 @@
     "apply": "Apply",
     "submit": "Submit",
     "more": "More",
+    "loading": "Loading…",
     "generic_error": "An error occured",
+    "error_retry": "Please try again",
+    "retry": "Try again",
     "optional": "optional",
     "show_more": "Show more",
     "show_less": "Show less",
@@ -68,7 +71,9 @@
     "disable": "Disable",
     "enable": "Enable",
     "confirm": "Confirm",
-    "verify": "Verify"
+    "verify": "Verify",
+    "close": "Close",
+    "peek": "Peek"
   },
   "image_cropper": {
     "crop_picture": "Crop picture",
@@ -278,6 +283,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/config.js b/src/modules/config.js
index 8f4638f5..b6b1b241 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -3,6 +3,16 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
 
 const browserLocale = (window.navigator.language || 'en').split('-')[0]
 
+/* TODO this is a bit messy.
+ * We need to declare settings with their types and also deal with
+ * instance-default settings in some way, hopefully try to avoid copy-pasta
+ * in general.
+ */
+export const multiChoiceProperties = [
+  'postContentType',
+  'subjectLineBehavior'
+]
+
 export const defaultState = {
   colors: {},
   theme: undefined,
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 5b2762e5..eeebd65e 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,6 +1,8 @@
 import { set, delete as del } from 'vue'
 
 const defaultState = {
+  settingsModalState: 'hidden',
+  settingsModalLoaded: false,
   settings: {
     currentSaveStateNotice: null,
     noticeClearTimeout: null,
@@ -35,6 +37,27 @@ 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'
+      if (!state.settingsModalLoaded) {
+        state.settingsModalLoaded = true
+      }
     }
   },
   actions: {
@@ -49,6 +72,15 @@ const interfaceMod = {
     },
     setMobileLayout ({ commit }, value) {
       commit('setMobileLayout', value)
+    },
+    closeSettingsModal ({ commit }) {
+      commit('closeSettingsModal')
+    },
+    openSettingsModal ({ commit }) {
+      commit('openSettingsModal')
+    },
+    togglePeekSettingsModal ({ commit }) {
+      commit('togglePeekSettingsModal')
     }
   }
 }
diff --git a/src/services/resettable_async_component.js b/src/services/resettable_async_component.js
new file mode 100644
index 00000000..517bbd88
--- /dev/null
+++ b/src/services/resettable_async_component.js
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+
+/* By default async components don't have any way to recover, if component is
+ * failed, it is failed forever. This helper tries to remedy that by recreating
+ * async component when retry is requested (by user). You need to emit the
+ * `resetAsyncComponent` event from child to reset the component. Generally,
+ * this should be done from error component but could be done from loading or
+ * actual target component itself if needs to be.
+ */
+function getResettableAsyncComponent (asyncComponent, options) {
+  const asyncComponentFactory = () => () => ({
+    component: asyncComponent(),
+    ...options
+  })
+
+  const observe = Vue.observable({ c: asyncComponentFactory() })
+
+  return {
+    functional: true,
+    render (createElement, { data, children }) {
+      //  emit event resetAsyncComponent to reloading
+      data.on = {}
+      data.on.resetAsyncComponent = () => {
+        observe.c = asyncComponentFactory()
+        // parent.$forceUpdate()
+      }
+      return createElement(observe.c, data, children)
+    }
+  }
+}
+
+export default getResettableAsyncComponent
diff --git a/static/fontello.json b/static/fontello.json
index 7f0e7cdd..ac3f0a18 100755
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -363,6 +363,18 @@
       "css": "ok",
       "code": 59431,
       "src": "fontawesome"
+    },
+    {
+      "uid": "4109c474ff99cad28fd5a2c38af2ec6f",
+      "css": "filter",
+      "code": 61616,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
+      "css": "download",
+      "code": 59429,
+      "src": "fontawesome"
     }
   ]
 }
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 0defefcb..61afa7ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2327,6 +2327,7 @@ dateformat@^1.0.6:
 de-indent@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+  integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
 
 debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
@@ -7903,9 +7904,10 @@ vue-style-loader@^4.0.0, vue-style-loader@^4.0.1:
     hash-sum "^1.0.2"
     loader-utils "^1.0.2"
 
-vue-template-compiler@^2.3.4:
-  version "2.5.21"
-  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a"
+vue-template-compiler@^2.6.11:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
+  integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
   dependencies:
     de-indent "^1.0.2"
     he "^1.1.0"
@@ -7914,9 +7916,10 @@ vue-template-es2015-compiler@^1.6.0:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
 
-vue@^2.5.13:
-  version "2.5.21"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85"
+vue@^2.6.11:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
+  integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
 
 vuelidate@^0.7.4:
   version "0.7.4"