diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1467f133..feabbf06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,28 +4,38 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ## [Unreleased]
 ### Changed
+- Greentext now has separate color slot for it
 - Removed the use of with_move parameters when fetching notifications
+- Push notifications now are the same as normal notfication, and are localized.
 
 ### Fixed
+- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully)
 - Multiple issues with muted statuses/notifications
 
 ## [Unreleased patch]
 ### Add
 - Added private notifications option for push notifications
 - 'Copy link' button for statuses (in the ellipsis menu)
+- Autocomplete domains from list of known instances
+- 'Bot' settings option and badge
 
 ### Changed
 - Registration page no longer requires email if the server is configured not to require it
 - Change heart to thumbs up in reaction picker
 - Close the media modal on navigation events
 - Add colons to the emoji alt text, to make them copyable
+- Add better visual indication for drag-and-drop for files
 
 ### Fixed
+- Custom Emoji will display in poll options now.
 - Status ellipsis menu closes properly when selecting certain options
 - Cropped images look correct in Chrome
 - Newlines in the muted words settings work again
 - Clicking on non-latin hashtags won't open a new window
 - Uploading and drag-dropping multiple files works correctly now.
+- Subject field now appears disabled when posting
+- Fix status ellipsis menu being cut off in notifications column
+- Fixed autocomplete sometimes not returning the right user when there's already some results
 
 ## [2.0.3] - 2020-05-02
 ### Fixed
diff --git a/package.json b/package.json
index 4d68cc6e..c0665f6e 100644
--- a/package.json
+++ b/package.json
@@ -22,12 +22,9 @@
     "cropperjs": "^1.4.3",
     "diff": "^3.0.1",
     "escape-html": "^1.0.3",
-    "karma-mocha-reporter": "^2.2.1",
     "localforage": "^1.5.0",
-    "object-path": "^0.11.3",
     "phoenix": "^1.3.0",
     "portal-vue": "^2.1.4",
-    "sanitize-html": "^1.13.0",
     "v-click-outside": "^2.1.1",
     "vue": "^2.6.11",
     "vue-chat-scroll": "^1.2.1",
@@ -35,10 +32,10 @@
     "vue-router": "^3.0.1",
     "vue-template-compiler": "^2.6.11",
     "vuelidate": "^0.7.4",
-    "vuex": "^3.0.1",
-    "whatwg-fetch": "^2.0.3"
+    "vuex": "^3.0.1"
   },
   "devDependencies": {
+    "karma-mocha-reporter": "^2.2.1",
     "@babel/core": "^7.7.5",
     "@babel/plugin-transform-runtime": "^7.7.6",
     "@babel/preset-env": "^7.7.6",
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 744b77d5..029e7096 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -3,6 +3,7 @@
     <Popover
       trigger="click"
       placement="bottom"
+      :bound-to="{ x: 'container' }"
     >
       <div
         slot="content"
diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js
index c8e838ba..f234dcb0 100644
--- a/src/components/domain_mute_card/domain_mute_card.js
+++ b/src/components/domain_mute_card/domain_mute_card.js
@@ -5,9 +5,20 @@ const DomainMuteCard = {
   components: {
     ProgressButton
   },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    muted () {
+      return this.user.domainMutes.includes(this.domain)
+    }
+  },
   methods: {
     unmuteDomain () {
       return this.$store.dispatch('unmuteDomain', this.domain)
+    },
+    muteDomain () {
+      return this.$store.dispatch('muteDomain', this.domain)
     }
   }
 }
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
index 567d81c5..97aee243 100644
--- a/src/components/domain_mute_card/domain_mute_card.vue
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -4,6 +4,7 @@
       {{ domain }}
     </div>
     <ProgressButton
+      v-if="muted"
       :click="unmuteDomain"
       class="btn btn-default"
     >
@@ -12,6 +13,16 @@
         {{ $t('domain_mute_card.unmute_progress') }}
       </template>
     </ProgressButton>
+    <ProgressButton
+      v-else
+      :click="muteDomain"
+      class="btn btn-default"
+    >
+      {{ $t('domain_mute_card.mute') }}
+      <template slot="progress">
+        {{ $t('domain_mute_card.mute_progress') }}
+      </template>
+    </ProgressButton>
   </div>
 </template>
 
@@ -34,5 +45,9 @@
   button {
     width: 10em;
   }
+
+  .autosuggest-results & {
+    padding-left: 1em;
+  }
 }
 </style>
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 15a71eff..8330345b 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -13,7 +13,7 @@ import { debounce } from 'lodash'
 
 const debounceUserSearch = debounce((data, input) => {
   data.updateUsersList(input)
-}, 500, { leading: true, trailing: false })
+}, 500)
 
 export default data => input => {
   const firstChar = input[0]
@@ -97,8 +97,8 @@ export const suggestUsers = data => input => {
     replacement: '@' + screen_name + ' '
   }))
 
-  // BE search users if there are no matches
-  if (newUsers.length === 0 && data.updateUsersList) {
+  // BE search users to get more comprehensive results
+  if (data.updateUsersList) {
     debounceUserSearch(data, noPrefix)
   }
   return newUsers
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index bca93ea7..68db6fd8 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -3,6 +3,7 @@
     trigger="click"
     placement="top"
     class="extra-button-popover"
+    :bound-to="{ x: 'container' }"
   >
     <div
       slot="content"
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index 7abc2161..1ffa7b3c 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -78,6 +78,7 @@
     video,
     canvas {
       object-fit: contain;
+      height: 100%;
     }
   }
 
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 5849b065..fbb2d03d 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -45,20 +45,6 @@ const mediaUpload = {
         this.$emit('all-uploaded')
       }
     },
-    fileDrop (e) {
-      if (e.dataTransfer.files.length > 0) {
-        e.preventDefault() // allow dropping text like before
-        this.multiUpload(e.dataTransfer.files)
-      }
-    },
-    fileDrag (e) {
-      let types = e.dataTransfer.types
-      if (types.contains('Files')) {
-        e.dataTransfer.dropEffect = 'copy'
-      } else {
-        e.dataTransfer.dropEffect = 'none'
-      }
-    },
     clearFile () {
       this.uploadReady = false
       this.$nextTick(() => {
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 0fc305ac..5e31730b 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,10 +1,5 @@
 <template>
-  <div
-    class="media-upload"
-    @drop.prevent
-    @dragover.prevent="fileDrag"
-    @drop="fileDrop"
-  >
+  <div class="media-upload">
     <label
       class="label"
       :title="$t('tool_tip.media_upload')"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index b675af5a..20797cf9 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -54,25 +54,20 @@
     flex-wrap: nowrap;
     padding: 0.6em;
     min-width: 0;
+
     .avatar-container {
       width: 32px;
       height: 32px;
     }
-    .status-el {
-      .status {
-        padding: 0.25em 0;
-        color: $fallback--faint;
-        color: var(--faint, $fallback--faint);
-        a {
-          color: var(--faintLink);
-        }
-        .status-content a {
-          color: var(--postFaintLink);
-        }
+
+    .status-body {
+      color: $fallback--faint;
+      color: var(--faint, $fallback--faint);
+      a {
+        color: var(--faintLink);
       }
-      padding: 0;
-      .media-body {
-        margin: 0;
+      .status-content a {
+        color: var(--postFaintLink);
       }
     }
   }
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
index 56e91cca..adbb0555 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -17,7 +17,7 @@
           <span class="result-percentage">
             {{ percentageForOption(option.votes_count) }}%
           </span>
-          <span>{{ option.title }}</span>
+          <span v-html="option.title_html"></span>
         </div>
         <div
           class="result-fill"
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 5881d266..a40a9195 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -1,4 +1,3 @@
-
 const Popover = {
   name: 'Popover',
   props: {
@@ -10,6 +9,9 @@ const Popover = {
     // 'container' for using offsetParent as boundaries for either axis
     // or 'viewport'
     boundTo: Object,
+    // Takes a selector to use as a replacement for the parent container
+    // for getting boundaries for x an y axis
+    boundToSelector: String,
     // Takes a top/bottom/left/right object, how much space to leave
     // between boundary and popover element
     margin: Object,
@@ -27,6 +29,10 @@ const Popover = {
     }
   },
   methods: {
+    containerBoundingClientRect () {
+      const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
+      return container.getBoundingClientRect()
+    },
     updateStyles () {
       if (this.hidden) {
         this.styles = {
@@ -45,7 +51,8 @@ const Popover = {
       // Minor optimization, don't call a slow reflow call if we don't have to
       const parentBounds = this.boundTo &&
         (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
-        this.$el.offsetParent.getBoundingClientRect()
+        this.containerBoundingClientRect()
+
       const margin = this.margin || {}
 
       // What are the screen bounds for the popover? Viewport vs container
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 6164caa0..9027566f 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -82,7 +82,9 @@ const PostStatusForm = {
         contentType
       },
       caret: 0,
-      pollFormVisible: false
+      pollFormVisible: false,
+      showDropIcon: 'hide',
+      dropStopTimeout: null
     }
   },
   computed: {
@@ -248,13 +250,27 @@ const PostStatusForm = {
       }
     },
     fileDrop (e) {
-      if (e.dataTransfer.files.length > 0) {
+      if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
         e.preventDefault() // allow dropping text like before
         this.dropFiles = e.dataTransfer.files
+        clearTimeout(this.dropStopTimeout)
+        this.showDropIcon = 'hide'
       }
     },
+    fileDragStop (e) {
+      // The false-setting is done with delay because just using leave-events
+      // directly caused unwanted flickering, this is not perfect either but
+      // much less noticable.
+      clearTimeout(this.dropStopTimeout)
+      this.showDropIcon = 'fade'
+      this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
+    },
     fileDrag (e) {
       e.dataTransfer.dropEffect = 'copy'
+      if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
+        clearTimeout(this.dropStopTimeout)
+        this.showDropIcon = 'show'
+      }
     },
     onEmojiInputInput (e) {
       this.$nextTick(() => {
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 5629ceac..e3d8d087 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -6,7 +6,15 @@
     <form
       autocomplete="off"
       @submit.prevent="postStatus(newStatus)"
+      @dragover.prevent="fileDrag"
     >
+      <div
+        v-show="showDropIcon !== 'hide'"
+        :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
+        class="drop-indicator icon-upload"
+        @dragleave="fileDragStop"
+        @drop.stop="fileDrop"
+      />
       <div class="form-group">
         <i18n
           v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
@@ -73,6 +81,7 @@
             v-model="newStatus.spoilerText"
             type="text"
             :placeholder="$t('post_status.content_warning')"
+            :disabled="posting"
             class="form-post-subject"
           >
         </EmojiInput>
@@ -96,9 +105,7 @@
             :disabled="posting"
             class="form-post-body"
             @keydown.meta.enter="postStatus(newStatus)"
-            @keyup.ctrl.enter="postStatus(newStatus)"
-            @drop="fileDrop"
-            @dragover.prevent="fileDrag"
+            @keydown.ctrl.enter="postStatus(newStatus)"
             @input="resize"
             @compositionupdate="resize"
             @paste="paste"
@@ -447,7 +454,8 @@
   form {
     display: flex;
     flex-direction: column;
-    padding: 0.6em;
+    margin: 0.6em;
+    position: relative;
   }
 
   .form-group {
@@ -505,5 +513,35 @@
     cursor: pointer;
     z-index: 4;
   }
+
+  @keyframes fade-in {
+    from { opacity: 0; }
+    to   { opacity: 0.6; }
+  }
+
+  @keyframes fade-out {
+    from { opacity: 0.6; }
+    to   { opacity: 0; }
+  }
+
+  .drop-indicator {
+    position: absolute;
+    z-index: 1;
+    width: 100%;
+    height: 100%;
+    font-size: 5em;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0.6;
+    color: $fallback--text;
+    color: var(--text, $fallback--text);
+    background-color: $fallback--bg;
+    background-color: var(--bg, $fallback--bg);
+    border-radius: $fallback--tooltipRadius;
+    border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+    border: 2px dashed $fallback--text;
+    border: 2px dashed 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 be945400..b44354db 100644
--- a/src/components/post_status_modal/post_status_modal.js
+++ b/src/components/post_status_modal/post_status_modal.js
@@ -13,6 +13,9 @@ 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 07c58f74..dbcd321e 100644
--- a/src/components/post_status_modal/post_status_modal.vue
+++ b/src/components/post_status_modal/post_status_modal.vue
@@ -1,5 +1,6 @@
 <template>
   <Modal
+    v-if="isLoggedIn && !resettingForm"
     :is-open="modalActivated"
     class="post-form-modal-view"
     @backdropClicked="closeModal"
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
index b0043dbb..40a87b81 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -32,12 +32,12 @@ const DomainMuteList = withSubscription({
 const MutesAndBlocks = {
   data () {
     return {
-      activeTab: 'profile',
-      newDomainToMute: ''
+      activeTab: 'profile'
     }
   },
   created () {
     this.$store.dispatch('fetchTokens')
+    this.$store.dispatch('getKnownDomains')
   },
   components: {
     TabSwitcher,
@@ -51,6 +51,14 @@ const MutesAndBlocks = {
     Autosuggest,
     Checkbox
   },
+  computed: {
+    knownDomains () {
+      return this.$store.state.instance.knownDomains
+    },
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
   methods: {
     importFollows (file) {
       return this.$store.state.api.backendInteractor.importFollows({ file })
@@ -86,13 +94,13 @@ const MutesAndBlocks = {
     filterUnblockedUsers (userIds) {
       return reject(userIds, (userId) => {
         const relationship = this.$store.getters.relationship(this.userId)
-        return relationship.blocking || userId === this.$store.state.users.currentUser.id
+        return relationship.blocking || userId === this.user.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
+        return relationship.muting || userId === this.user.id
       })
     },
     queryUserIds (query) {
@@ -111,12 +119,16 @@ const MutesAndBlocks = {
     unmuteUsers (ids) {
       return this.$store.dispatch('unmuteUsers', ids)
     },
+    filterUnMutedDomains (urls) {
+      return urls.filter(url => !this.user.domainMutes.includes(url))
+    },
+    queryKnownDomains (query) {
+      return new Promise((resolve, reject) => {
+        resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query)))
+      })
+    },
     unmuteDomains (domains) {
       return this.$store.dispatch('unmuteDomains', domains)
-    },
-    muteDomain () {
-      return this.$store.dispatch('muteDomain', this.newDomainToMute)
-        .then(() => { this.newDomainToMute = '' })
     }
   }
 }
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
index 6884b7be..5a1cf2c0 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -119,21 +119,16 @@
 
         <div :label="$t('settings.domain_mutes')">
           <div class="domain-mute-form">
-            <input
-              v-model="newDomainToMute"
+            <Autosuggest
+              :filter="filterUnMutedDomains"
+              :query="queryKnownDomains"
               :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>
+              <DomainMuteCard
+                slot-scope="row"
+                :domain="row.item"
+              />
+            </Autosuggest>
           </div>
           <DomainMuteList
             :refresh="true"
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 0874e0c8..e6db802d 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -25,6 +25,7 @@ const ProfileTab = {
       showRole: this.$store.state.users.currentUser.show_role,
       role: this.$store.state.users.currentUser.role,
       discoverable: this.$store.state.users.currentUser.discoverable,
+      bot: this.$store.state.users.currentUser.bot,
       allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
       pickAvatarBtnVisible: true,
       bannerUploading: false,
@@ -94,6 +95,7 @@ const ProfileTab = {
             hide_follows: this.hideFollows,
             hide_followers: this.hideFollowers,
             discoverable: this.discoverable,
+            bot: this.bot,
             allow_following_move: this.allowFollowingMove,
             hide_follows_count: this.hideFollowsCount,
             hide_followers_count: this.hideFollowersCount,
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 20a68f7d..0f9210a6 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -143,6 +143,11 @@
           {{ $t("settings.profile_fields.add_field") }}
         </a>
       </div>
+      <p>
+        <Checkbox v-model="bot">
+          {{ $t('settings.bot') }}
+        </Checkbox>
+      </p>
       <button
         :disabled="newName && newName.length === 0"
         class="btn btn-default"
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index fcfad23b..d14f854c 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -256,6 +256,13 @@
               :label="$t('settings.links')"
             />
             <ContrastRatio :contrast="previewContrast.postLink" />
+            <ColorInput
+              v-model="postGreentextColorLocal"
+              name="postGreentextColor"
+              :fallback="previewTheme.colors.cGreen"
+              :label="$t('settings.greentext')"
+            />
+            <ContrastRatio :contrast="previewContrast.postGreentext" />
             <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
             <ColorInput
               v-model="alertErrorColorLocal"
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 336f912a..7ec29b28 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -418,7 +418,7 @@ $status-margin: 0.75em;
     max-width: 85%;
     font-weight: bold;
 
-    img {
+    img.emoji {
       width: 14px;
       height: 14px;
       vertical-align: middle;
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 8c2e8749..efc2485e 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -164,23 +164,23 @@ $status-margin: 0.75em;
     word-break: break-all;
   }
 
+  img, video {
+    max-width: 100%;
+    max-height: 400px;
+    vertical-align: middle;
+    object-fit: contain;
+
+    &.emoji {
+      width: 32px;
+      height: 32px;
+    }
+  }
+
   .status-content {
     font-family: var(--postFont, sans-serif);
     line-height: 1.4em;
     white-space: pre-wrap;
 
-    img, video {
-      max-width: 100%;
-      max-height: 400px;
-      vertical-align: middle;
-      object-fit: contain;
-
-      &.emoji {
-        width: 32px;
-        height: 32px;
-      }
-    }
-
     blockquote {
       margin: 0.2em 0 0.2em 2em;
       font-style: italic;
@@ -226,7 +226,7 @@ $status-margin: 0.75em;
 
 .greentext {
   color: $fallback--cGreen;
-  color: var(--cGreen, $fallback--cGreen);
+  color: var(--postGreentext, $fallback--cGreen);
 }
 
 .timeline :not(.panel-disabled) > {
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index 08af26f6..f2ddeb7b 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -23,13 +23,6 @@
 
 <style lang="scss">
 @import '../../_variables.scss';
-.contain-fit {
-  .still-image {
-    img {
-      height: 100%;
-    }
-  }
-}
 
 .still-image {
   position: relative;
@@ -38,6 +31,7 @@
   width: 100%;
   height: 100%;
   display: flex;
+  align-items: center;
 
   &:hover canvas {
     display: none;
@@ -45,8 +39,8 @@
 
   img {
     width: 100%;
+    min-height: 100%;
     object-fit: contain;
-    align-self: center;
   }
 
   &.animated {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index c4a5ce9d..9529d7f6 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -70,10 +70,20 @@
               >
                 @{{ user.screen_name }}
               </router-link>
-              <span
-                v-if="!hideBio && !!visibleRole"
-                class="alert staff"
-              >{{ visibleRole }}</span>
+              <template v-if="!hideBio">
+                <span
+                  v-if="!!visibleRole"
+                  class="alert user-role"
+                >
+                  {{ visibleRole }}
+                </span>
+                <span
+                  v-if="user.bot"
+                  class="alert user-role"
+                >
+                  bot
+                </span>
+              </template>
               <span v-if="user.locked"><i class="icon icon-lock" /></span>
               <span
                 v-if="!mergedConfig.hideUserStats && !hideBio"
@@ -458,7 +468,7 @@
       color: var(--text, $fallback--text);
     }
 
-    .staff {
+    .user-role {
       flex: none;
       text-transform: capitalize;
       color: $fallback--text;
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 95760bf8..201727d4 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -124,6 +124,14 @@ const UserProfile = {
     onTabSwitch (tab) {
       this.tab = tab
       this.$router.replace({ query: { tab } })
+    },
+    linkClicked ({ target }) {
+      if (target.tagName === 'SPAN') {
+        target = target.parentNode
+      }
+      if (target.tagName === 'A') {
+        window.open(target.href, '_blank')
+      }
     }
   },
   watch: {
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 1871d46c..361a3b5c 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -11,6 +11,31 @@
         :allow-zooming-avatar="true"
         rounded="top"
       />
+      <div
+        v-if="user.fields_html && user.fields_html.length > 0"
+        class="user-profile-fields"
+      >
+        <dl
+          v-for="(field, index) in user.fields_html"
+          :key="index"
+          class="user-profile-field"
+        >
+          <!-- eslint-disable vue/no-v-html -->
+          <dt
+            :title="user.fields_text[index].name"
+            class="user-profile-field-name"
+            @click.prevent="linkClicked"
+            v-html="field.name"
+          />
+          <dd
+            :title="user.fields_text[index].value"
+            class="user-profile-field-value"
+            @click.prevent="linkClicked"
+            v-html="field.value"
+          />
+          <!-- eslint-enable vue/no-v-html -->
+        </dl>
+      </div>
       <tab-switcher
         :active-tab="tab"
         :render-only-focused="true"
@@ -108,11 +133,60 @@
 <script src="./user_profile.js"></script>
 
 <style lang="scss">
+@import '../../_variables.scss';
 
 .user-profile {
   flex: 2;
   flex-basis: 500px;
 
+  .user-profile-fields {
+    margin: 0 0.5em;
+    img {
+      object-fit: contain;
+      vertical-align: middle;
+      max-width: 100%;
+      max-height: 400px;
+
+      &.emoji {
+        width: 18px;
+        height: 18px;
+      }
+    }
+
+    .user-profile-field {
+      display: flex;
+      margin: 0.25em auto;
+      max-width: 32em;
+      border: 1px solid var(--border, $fallback--border);
+      border-radius: $fallback--inputRadius;
+      border-radius: var(--inputRadius, $fallback--inputRadius);
+
+      .user-profile-field-name {
+        flex: 0 1 30%;
+        font-weight: 500;
+        text-align: right;
+        color: var(--lightText);
+        min-width: 120px;
+        border-right: 1px solid var(--border, $fallback--border);
+      }
+
+      .user-profile-field-value {
+        flex: 1 1 70%;
+        color: var(--text);
+        margin: 0 0 0 0.25em;
+      }
+
+      .user-profile-field-name, .user-profile-field-value {
+        line-height: 18px;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        overflow: hidden;
+        padding: 0.5em 1.5em;
+        box-sizing: border-box;
+      }
+    }
+  }
+
   .userlist-placeholder {
     display: flex;
     justify-content: center;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ca49514e..2840904f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -266,6 +266,7 @@
     "block_import_error": "Error importing blocks",
     "blocks_imported": "Blocks imported! Processing them will take a while.",
     "blocks_tab": "Blocks",
+    "bot": "This is a bot account",
     "btnRadius": "Buttons",
     "cBlue": "Blue (Reply, follow)",
     "cGreen": "Green (Retweet)",
@@ -407,7 +408,7 @@
     "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
     "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
     "tooltipRadius": "Tooltips/alerts",
-    "type_domains_to_mute": "Type in domains to mute",
+    "type_domains_to_mute": "Search domains to mute",
     "upload_a_photo": "Upload a photo",
     "user_settings": "User Settings",
     "values": {
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 360c72aa..6c8be351 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -12,7 +12,12 @@
     "disable": "Disabilita",
     "enable": "Abilita",
     "confirm": "Conferma",
-    "verify": "Verifica"
+    "verify": "Verifica",
+    "peek": "Anteprima",
+    "close": "Chiudi",
+    "retry": "Riprova",
+    "error_retry": "Per favore, riprova",
+    "loading": "Carico…"
   },
   "nav": {
     "mentions": "Menzioni",
@@ -212,7 +217,63 @@
       },
       "common": {
         "opacity": "Opacità",
-        "color": "Colore"
+        "color": "Colore",
+        "contrast": {
+          "context": {
+            "text": "per il testo",
+            "18pt": "per il testo grande (oltre 17pt)"
+          },
+          "level": {
+            "bad": "non soddisfa le linee guida di alcun livello",
+            "aaa": "soddisfa le linee guida di livello AAA (ottimo)",
+            "aa": "soddisfa le linee guida di livello AA (sufficiente)"
+          },
+          "hint": "Il rapporto di contrasto è {ratio}, e {level} {context}"
+        }
+      },
+      "advanced_colors": {
+        "badge": "Sfondo medaglie",
+        "post": "Messaggi / Biografie",
+        "alert_neutral": "Neutro",
+        "alert_warning": "Attenzione",
+        "alert_error": "Errore",
+        "alert": "Sfondo degli avvertimenti",
+        "_tab_label": "Avanzate",
+        "tabs": "Etichette",
+        "disabled": "Disabilitato",
+        "selectedMenu": "Voce menù selezionata",
+        "selectedPost": "Messaggio selezionato",
+        "pressed": "Premuto",
+        "highlight": "Elementi evidenziati",
+        "icons": "Icone",
+        "poll": "Grafico sondaggi",
+        "underlay": "Sottostante",
+        "faint_text": "Testo sbiadito",
+        "inputs": "Campi d'immissione",
+        "buttons": "Pulsanti",
+        "borders": "Bordi",
+        "top_bar": "Barra superiore",
+        "panel_header": "Titolo pannello",
+        "badge_notification": "Notifica",
+        "popover": "Suggerimenti, menù, sbalzi"
+      },
+      "common_colors": {
+        "rgbo": "Icone, accenti, medaglie",
+        "foreground_hint": "Seleziona l'etichetta \"Avanzate\" per controlli più fini",
+        "main": "Colori comuni",
+        "_tab_label": "Comuni"
+      },
+      "shadows": {
+        "inset": "Includi",
+        "spread": "Spandi",
+        "blur": "Sfoca",
+        "shadow_id": "Ombra numero {value}",
+        "override": "Sostituisci",
+        "component": "Componente",
+        "_tab_label": "Luci ed ombre"
+      },
+      "radii": {
+        "_tab_label": "Raggio"
       }
     },
     "enable_web_push_notifications": "Abilita notifiche web push",
@@ -229,7 +290,7 @@
     "notifications": "Notifiche",
     "greentext": "Frecce da meme",
     "upload_a_photo": "Carica un'immagine",
-    "type_domains_to_mute": "Inserisci domini da zittire",
+    "type_domains_to_mute": "Cerca domini da zittire",
     "theme_help_v2_2": "Le icone dietro alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se si usano delle trasparenze, questi indicatori mostrano il peggior caso possibile.",
     "theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.",
     "useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)",
@@ -273,7 +334,8 @@
     "accent": "Accento",
     "emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze",
     "pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore",
-    "notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più."
+    "notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.",
+    "mutes_and_blocks": "Zittiti e bloccati"
   },
   "timeline": {
     "error_fetching": "Errore nell'aggiornamento",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 69b22618..aa78db26 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -130,6 +130,7 @@
     "background": "Фон",
     "bio": "Описание",
     "btnRadius": "Кнопки",
+    "bot": "Это аккаунт бота",
     "cBlue": "Ответить, читать",
     "cGreen": "Повторить",
     "cOrange": "Нравится",
@@ -456,9 +457,9 @@
   },
   "domain_mute_card": {
     "mute": "Игнорировать",
-    "mute_progress": "В процессе...",
+    "mute_progress": "В процессе…",
     "unmute": "Прекратить игнорирование",
-    "unmute_progress": "В процессе..."
+    "unmute_progress": "В процессе…"
   },
   "exporter": {
     "export": "Экспорт",
diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js
new file mode 100644
index 00000000..270ed043
--- /dev/null
+++ b/src/i18n/service_worker_messages.js
@@ -0,0 +1,35 @@
+/* eslint-disable import/no-webpack-loader-syntax */
+// This module exports only the notification part of the i18n,
+// which is useful for the service worker
+
+const messages = {
+  ar: require('../lib/notification-i18n-loader.js!./ar.json'),
+  ca: require('../lib/notification-i18n-loader.js!./ca.json'),
+  cs: require('../lib/notification-i18n-loader.js!./cs.json'),
+  de: require('../lib/notification-i18n-loader.js!./de.json'),
+  eo: require('../lib/notification-i18n-loader.js!./eo.json'),
+  es: require('../lib/notification-i18n-loader.js!./es.json'),
+  et: require('../lib/notification-i18n-loader.js!./et.json'),
+  eu: require('../lib/notification-i18n-loader.js!./eu.json'),
+  fi: require('../lib/notification-i18n-loader.js!./fi.json'),
+  fr: require('../lib/notification-i18n-loader.js!./fr.json'),
+  ga: require('../lib/notification-i18n-loader.js!./ga.json'),
+  he: require('../lib/notification-i18n-loader.js!./he.json'),
+  hu: require('../lib/notification-i18n-loader.js!./hu.json'),
+  it: require('../lib/notification-i18n-loader.js!./it.json'),
+  ja: require('../lib/notification-i18n-loader.js!./ja_pedantic.json'),
+  ja_easy: require('../lib/notification-i18n-loader.js!./ja_easy.json'),
+  ko: require('../lib/notification-i18n-loader.js!./ko.json'),
+  nb: require('../lib/notification-i18n-loader.js!./nb.json'),
+  nl: require('../lib/notification-i18n-loader.js!./nl.json'),
+  oc: require('../lib/notification-i18n-loader.js!./oc.json'),
+  pl: require('../lib/notification-i18n-loader.js!./pl.json'),
+  pt: require('../lib/notification-i18n-loader.js!./pt.json'),
+  ro: require('../lib/notification-i18n-loader.js!./ro.json'),
+  ru: require('../lib/notification-i18n-loader.js!./ru.json'),
+  te: require('../lib/notification-i18n-loader.js!./te.json'),
+  zh: require('../lib/notification-i18n-loader.js!./zh.json'),
+  en: require('../lib/notification-i18n-loader.js!./en.json')
+}
+
+export default messages
diff --git a/src/lib/notification-i18n-loader.js b/src/lib/notification-i18n-loader.js
new file mode 100644
index 00000000..71f9156a
--- /dev/null
+++ b/src/lib/notification-i18n-loader.js
@@ -0,0 +1,12 @@
+// This somewhat mysterious module will load a json string
+// and then extract only the 'notifications' part. This is
+// meant to be used to load the partial i18n we need for
+// the service worker.
+module.exports = function (source) {
+  var object = JSON.parse(source)
+  var smol = {
+    notifications: object.notifications || {}
+  }
+
+  return JSON.stringify(smol)
+}
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index cad7ea25..8ecb66a8 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -1,13 +1,12 @@
 import merge from 'lodash.merge'
-import objectPath from 'object-path'
 import localforage from 'localforage'
-import { each } from 'lodash'
+import { each, get, set } from 'lodash'
 
 let loaded = false
 
 const defaultReducer = (state, paths) => (
   paths.length === 0 ? state : paths.reduce((substate, path) => {
-    objectPath.set(substate, path, objectPath.get(state, path))
+    set(substate, path, get(state, path))
     return substate
   }, {})
 )
diff --git a/src/modules/instance.js b/src/modules/instance.js
index da82eb01..ec5f4e54 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,6 +1,7 @@
 import { set } from 'vue'
 import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
 import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
+import apiService from '../services/api/api.service.js'
 import { instanceDefaultProperties } from './config.js'
 
 const defaultState = {
@@ -48,6 +49,7 @@ const defaultState = {
   postFormats: [],
   restrictedNicknames: [],
   safeDM: true,
+  knownDomains: [],
 
   // Feature-set, apparently, not everything here is reported...
   chatAvailable: false,
@@ -80,6 +82,9 @@ const instance = {
       if (typeof value !== 'undefined') {
         set(state, name, value)
       }
+    },
+    setKnownDomains (state, domains) {
+      state.knownDomains = domains
     }
   },
   getters: {
@@ -182,6 +187,18 @@ const instance = {
         state.emojiFetched = true
         dispatch('getStaticEmoji')
       }
+    },
+
+    async getKnownDomains ({ commit, rootState }) {
+      try {
+        const result = await apiService.fetchKnownDomains({
+          credentials: rootState.users.currentUser.credentials
+        })
+        commit('setKnownDomains', result)
+      } catch (e) {
+        console.warn("Can't load known domains")
+        console.warn(e)
+      }
     }
   }
 }
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 9a2e0df1..073b15f1 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,7 +13,7 @@ import {
   omitBy
 } from 'lodash'
 import { set } from 'vue'
-import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
+import { isStatusNotification, prepareNotificationObject } from '../services/notification_utils/notification_utils.js'
 import apiService from '../services/api/api.service.js'
 import { muteWordHits } from '../services/status_parser/status_parser.js'
 
@@ -344,42 +344,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
       state.notifications.idStore[notification.id] = notification
 
       if ('Notification' in window && window.Notification.permission === 'granted') {
-        const notifObj = {}
-        const status = notification.status
-        const title = notification.from_profile.name
-        notifObj.icon = notification.from_profile.profile_image_url
-        let i18nString
-        switch (notification.type) {
-          case 'like':
-            i18nString = 'favorited_you'
-            break
-          case 'repeat':
-            i18nString = 'repeated_you'
-            break
-          case 'follow':
-            i18nString = 'followed_you'
-            break
-          case 'move':
-            i18nString = 'migrated_to'
-            break
-          case 'follow_request':
-            i18nString = 'follow_request'
-            break
-        }
-
-        if (notification.type === 'pleroma:emoji_reaction') {
-          notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
-        } else if (i18nString) {
-          notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
-        } else if (isStatusNotification(notification.type)) {
-          notifObj.body = notification.status.text
-        }
-
-        // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
-        if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
-          status.attachments[0].mimetype.startsWith('image/')) {
-          notifObj.image = status.attachments[0].url
-        }
+        const notifObj = prepareNotificationObject(notification, rootGetters.i18n)
 
         const reasonsToMuteNotif = (
           notification.seen ||
@@ -393,7 +358,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
             )
         )
         if (!reasonsToMuteNotif) {
-          let desktopNotification = new window.Notification(title, notifObj)
+          let desktopNotification = new window.Notification(notifObj.title, notifObj)
           // Chrome is known for not closing notifications automatically
           // according to MDN, anyway.
           setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
diff --git a/src/modules/users.js b/src/modules/users.js
index fca01a56..68d02931 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -435,10 +435,10 @@ const users = {
         store.commit('setUserForNotification', notification)
       })
     },
-    searchUsers (store, { query }) {
-      return store.rootState.api.backendInteractor.searchUsers({ query })
+    searchUsers ({ rootState, commit }, { query }) {
+      return rootState.api.backendInteractor.searchUsers({ query })
         .then((users) => {
-          store.commit('addNewUsers', users)
+          commit('addNewUsers', users)
           return users
         })
     },
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 9c7530a2..dfffc291 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,6 +1,5 @@
 import { each, map, concat, last, get } from 'lodash'
 import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
-import 'whatwg-fetch'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
@@ -75,6 +74,7 @@ const MASTODON_SEARCH_2 = `/api/v2/search`
 const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
 const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_STREAMING = '/api/v1/streaming'
+const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
 const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
 const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
 const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
@@ -995,6 +995,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
     })
 }
 
+const fetchKnownDomains = ({ credentials }) => {
+  return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
+}
+
 const fetchDomainMutes = ({ credentials }) => {
   return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
 }
@@ -1193,6 +1197,7 @@ const apiService = {
   updateNotificationSettings,
   search2,
   searchUsers,
+  fetchKnownDomains,
   fetchDomainMutes,
   muteDomain,
   unmuteDomain
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index c7ed65a4..3bdb92f3 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -56,6 +56,12 @@ export const parseUser = (data) => {
         value: addEmojis(field.value, data.emojis)
       }
     })
+    output.fields_text = data.fields.map(field => {
+      return {
+        name: unescape(field.name.replace(/<[^>]*>/g, '')),
+        value: unescape(field.value.replace(/<[^>]*>/g, ''))
+      }
+    })
 
     // Utilize avatar_static for gif avatars?
     output.profile_image_url = data.avatar
@@ -258,6 +264,12 @@ export const parseStatus = (data) => {
     output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
     output.external_url = data.url
     output.poll = data.poll
+    if (output.poll) {
+      output.poll.options = (output.poll.options || []).map(field => ({
+        ...field,
+        title_html: addEmojis(field.title, data.emojis)
+      }))
+    }
     output.pinned = data.pinned
     output.muted = data.muted
   } else {
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index eb479227..5cc19215 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -43,3 +43,47 @@ export const filteredNotificationsFromStore = (store, types) => {
 
 export const unseenNotificationsFromStore = store =>
   filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
+
+export const prepareNotificationObject = (notification, i18n) => {
+  const notifObj = {
+    tag: notification.id
+  }
+  const status = notification.status
+  const title = notification.from_profile.name
+  notifObj.title = title
+  notifObj.icon = notification.from_profile.profile_image_url
+  let i18nString
+  switch (notification.type) {
+    case 'like':
+      i18nString = 'favorited_you'
+      break
+    case 'repeat':
+      i18nString = 'repeated_you'
+      break
+    case 'follow':
+      i18nString = 'followed_you'
+      break
+    case 'move':
+      i18nString = 'migrated_to'
+      break
+    case 'follow_request':
+      i18nString = 'follow_request'
+      break
+  }
+
+  if (notification.type === 'pleroma:emoji_reaction') {
+    notifObj.body = i18n.t('notifications.reacted_with', [notification.emoji])
+  } else if (i18nString) {
+    notifObj.body = i18n.t('notifications.' + i18nString)
+  } else if (isStatusNotification(notification.type)) {
+    notifObj.body = notification.status.text
+  }
+
+  // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
+  if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
+    status.attachments[0].mimetype.startsWith('image/')) {
+    notifObj.image = status.attachments[0].url
+  }
+
+  return notifObj
+}
diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js
index 3d517e3c..ed0f6d57 100644
--- a/src/services/status_parser/status_parser.js
+++ b/src/services/status_parser/status_parser.js
@@ -1,17 +1,4 @@
 import { filter } from 'lodash'
-import sanitize from 'sanitize-html'
-
-export const removeAttachmentLinks = (html) => {
-  return sanitize(html, {
-    allowedTags: false,
-    allowedAttributes: false,
-    exclusiveFilter: ({ tag, attribs }) => tag === 'a' && typeof attribs.class === 'string' && attribs.class.match(/attachment/)
-  })
-}
-
-export const parse = (html) => {
-  return removeAttachmentLinks(html)
-}
 
 export const muteWordHits = (status, muteWords) => {
   const statusText = status.text.toLowerCase()
@@ -22,5 +9,3 @@ export const muteWordHits = (status, muteWords) => {
 
   return hits
 }
-
-export default parse
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 0c1fe543..b577cfab 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -356,6 +356,12 @@ export const SLOT_INHERITANCE = {
     textColor: 'preserve'
   },
 
+  postGreentext: {
+    depends: ['cGreen'],
+    layer: 'bg',
+    textColor: 'preserve'
+  },
+
   border: {
     depends: ['fg'],
     opacity: 'border',
diff --git a/src/sw.js b/src/sw.js
index 6cecb3f3..f5e34dd6 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -1,6 +1,19 @@
 /* eslint-env serviceworker */
 
 import localForage from 'localforage'
+import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
+import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
+import Vue from 'vue'
+import VueI18n from 'vue-i18n'
+import messages from './i18n/service_worker_messages.js'
+
+Vue.use(VueI18n)
+const i18n = new VueI18n({
+  // By default, use the browser locale, we will update it if neccessary
+  locale: 'en',
+  fallbackLocale: 'en',
+  messages
+})
 
 function isEnabled () {
   return localForage.getItem('vuex-lz')
@@ -12,15 +25,33 @@ function getWindowClients () {
     .then((clientList) => clientList.filter(({ type }) => type === 'window'))
 }
 
-self.addEventListener('push', (event) => {
-  if (event.data) {
-    event.waitUntil(isEnabled().then((isEnabled) => {
-      return isEnabled && getWindowClients().then((list) => {
-        const data = event.data.json()
+const setLocale = async () => {
+  const state = await localForage.getItem('vuex-lz')
+  const locale = state.config.interfaceLanguage || 'en'
+  i18n.locale = locale
+}
 
-        if (list.length === 0) return self.registration.showNotification(data.title, data)
-      })
-    }))
+const maybeShowNotification = async (event) => {
+  const enabled = await isEnabled()
+  const activeClients = await getWindowClients()
+  await setLocale()
+  if (enabled && (activeClients.length === 0)) {
+    const data = event.data.json()
+
+    const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}`
+    const notification = await fetch(url, { headers: { Authorization: 'Bearer ' + data.access_token } })
+    const notificationJson = await notification.json()
+    const parsedNotification = parseNotification(notificationJson)
+
+    const res = prepareNotificationObject(parsedNotification, i18n)
+
+    self.registration.showNotification(res.title, res)
+  }
+}
+
+self.addEventListener('push', async (event) => {
+  if (event.data) {
+    event.waitUntil(maybeShowNotification(event))
   }
 })
 
diff --git a/static/terms-of-service.html b/static/terms-of-service.html
index a6da539e..b2c66815 100644
--- a/static/terms-of-service.html
+++ b/static/terms-of-service.html
@@ -1,4 +1,9 @@
 <h4>Terms of Service</h4>
 
-<p>This is a placeholder ToS. Edit <code>"/static/terms-of-service.html"</code> to make it fit the needs of your instance.</p>
+<p>This is the default placeholder ToS.  You should copy it over to your static folder and edit it to fit the needs of your instance.</p>
+
+<p>To do so, place a file at <code>"/instance/static/terms-of-service.html"</code> in your
+   Pleroma install containing the real ToS for your instance.</p>
+<p>See the <a href='https://docs.pleroma.social/backend/configuration/static_dir/'>Pleroma documentation</a> for more information.</p>
+<br>
 <img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />
diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json
index 7a4a29da..24480d2c 100644
--- a/static/themes/redmond-xx-se.json
+++ b/static/themes/redmond-xx-se.json
@@ -286,7 +286,9 @@
       "cGreen": "#008000",
       "cOrange": "#808000",
       "highlight": "--accent",
-      "selectedPost": "--bg,-10"
+      "selectedPost": "--bg,-10",
+      "selectedMenu": "--accent",
+      "selectedMenuPopover": "--accent"
     },
     "radii": {
       "btn": "0",
diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json
index ff95b1e0..cf9010fe 100644
--- a/static/themes/redmond-xx.json
+++ b/static/themes/redmond-xx.json
@@ -277,7 +277,9 @@
       "cGreen": "#008000",
       "cOrange": "#808000",
       "highlight": "--accent",
-      "selectedPost": "--bg,-10"
+      "selectedPost": "--bg,-10",
+      "selectedMenu": "--accent",
+      "selectedMenuPopover": "--accent"
     },
     "radii": {
       "btn": "0",
diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json
index f788bdb8..7fdc4a6d 100644
--- a/static/themes/redmond-xxi.json
+++ b/static/themes/redmond-xxi.json
@@ -259,7 +259,9 @@
       "cGreen": "#669966",
       "cOrange": "#cc6633",
       "highlight": "--accent",
-      "selectedPost": "--bg,-10"
+      "selectedPost": "--bg,-10",
+      "selectedMenu": "--accent",
+      "selectedMenuPopover": "--accent"
     },
     "radii": {
       "btn": "0",
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index 166fce2b..ccb57942 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -290,6 +290,19 @@ describe('API Entities normalizer', () => {
       expect(field).to.have.property('value').that.contains('<img')
     })
 
+    it('removes html tags from user profile fields', () => {
+      const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
+
+      const parsedUser = parseUser(user)
+
+      expect(parsedUser).to.have.property('fields_text').to.be.an('array')
+
+      const field = parsedUser.fields_text[0]
+
+      expect(field).to.have.property('name').that.equal('user')
+      expect(field).to.have.property('value').that.equal('@user')
+    })
+
     it('adds hide_follows and hide_followers user settings', () => {
       const user = makeMockUserMasto({ pleroma: { hide_followers: true, hide_follows: false, hide_followers_count: false, hide_follows_count: true } })
 
diff --git a/test/unit/specs/services/status_parser/status_parses.spec.js b/test/unit/specs/services/status_parser/status_parses.spec.js
deleted file mode 100644
index 7afd5042..00000000
--- a/test/unit/specs/services/status_parser/status_parses.spec.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { removeAttachmentLinks } from '../../../../../src/services/status_parser/status_parser.js'
-
-const example = '<div class="status-content">@<a href="https://sealion.club/user/4" class="h-card mention" title="dewoo">dwmatiz</a> <a href="https://social.heldscal.la/file/3deb764ada10ce64a61b7a070b75dac45f86d2d5bf213bf18873da71d8714d86.png" title="https://social.heldscal.la/file/3deb764ada10ce64a61b7a070b75dac45f86d2d5bf213bf18873da71d8714d86.png" class="attachment" id="attachment-159853" rel="nofollow external">https://social.heldscal.la/attachment/159853</a></div>'
-
-describe('statusParser.removeAttachmentLinks', () => {
-  const exampleWithoutAttachmentLinks = '<div class="status-content">@<a href="https://sealion.club/user/4" class="h-card mention" title="dewoo">dwmatiz</a> </div>'
-
-  it('removes attachment links', () => {
-    const parsed = removeAttachmentLinks(example)
-    expect(parsed).to.eql(exampleWithoutAttachmentLinks)
-  })
-
-  it('works when the class is empty', () => {
-    const parsed = removeAttachmentLinks('<a></a>')
-    expect(parsed).to.eql('<a></a>')
-  })
-})
diff --git a/yarn.lock b/yarn.lock
index 61afa7ca..f05b00b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1062,7 +1062,7 @@ array-union@^1.0.1:
   dependencies:
     array-uniq "^1.0.1"
 
-array-uniq@^1.0.1, array-uniq@^1.0.2:
+array-uniq@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
 
@@ -2545,7 +2545,7 @@ domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
 
-domelementtype@1, domelementtype@^1.3.0:
+domelementtype@1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
 
@@ -2559,12 +2559,6 @@ domhandler@2.1:
   dependencies:
     domelementtype "1"
 
-domhandler@^2.3.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
-  dependencies:
-    domelementtype "1"
-
 domutils@1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
@@ -2578,13 +2572,6 @@ domutils@1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
-domutils@^1.5.1:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
-  dependencies:
-    dom-serializer "0"
-    domelementtype "1"
-
 duplexer2@~0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -2711,7 +2698,7 @@ ent@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
 
-entities@^1.1.1, entities@~1.1.1:
+entities@~1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
 
@@ -3762,17 +3749,6 @@ html-webpack-plugin@^3.0.0, html-webpack-plugin@^3.2.0:
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
-htmlparser2@^3.10.0:
-  version "3.10.0"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
-  dependencies:
-    domelementtype "^1.3.0"
-    domhandler "^2.3.0"
-    domutils "^1.5.1"
-    entities "^1.1.1"
-    inherits "^2.0.1"
-    readable-stream "^3.0.6"
-
 htmlparser2@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
@@ -4757,10 +4733,6 @@ lodash.clone@3.0.3:
     lodash._bindcallback "^3.0.0"
     lodash._isiterateecall "^3.0.0"
 
-lodash.clonedeep@^4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
-
 lodash.create@3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
@@ -4780,10 +4752,6 @@ lodash.defaultsdeep@4.3.2:
     lodash.mergewith "^4.0.0"
     lodash.rest "^4.0.0"
 
-lodash.escaperegexp@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
-
 lodash.find@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-3.2.1.tgz#046e319f3ace912ac6c9246c7f683c5ec07b36ad"
@@ -4815,14 +4783,10 @@ lodash.isplainobject@^3.0.0, lodash.isplainobject@^3.2.0:
     lodash.isarguments "^3.0.0"
     lodash.keysin "^3.0.0"
 
-lodash.isplainobject@^4.0.0, lodash.isplainobject@^4.0.6:
+lodash.isplainobject@^4.0.0:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
 
-lodash.isstring@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
-
 lodash.istypedarray@^3.0.0:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62"
@@ -4871,7 +4835,7 @@ lodash.merge@^3.3.2:
     lodash.keysin "^3.0.0"
     lodash.toplainobject "^3.0.0"
 
-lodash.mergewith@^4.0.0, lodash.mergewith@^4.6.1:
+lodash.mergewith@^4.0.0:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
 
@@ -5538,10 +5502,6 @@ object-keys@^1.0.11, object-keys@^1.0.12:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
 
-object-path@^0.11.3:
-  version "0.11.4"
-  resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949"
-
 object-visit@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@@ -6245,14 +6205,6 @@ postcss@^7.0.0:
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
-postcss@^7.0.5:
-  version "7.0.8"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.8.tgz#2a3c5f2bdd00240cd0d0901fd998347c93d36696"
-  dependencies:
-    chalk "^2.4.2"
-    source-map "^0.6.1"
-    supports-color "^6.0.0"
-
 prelude-ls@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -6521,14 +6473,6 @@ readable-stream@1.1.x:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^3.0.6:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
-  dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
-
 readdirp@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -6839,21 +6783,6 @@ samsam@1.x, samsam@^1.1.3:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
 
-sanitize-html@^1.13.0:
-  version "1.20.0"
-  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156"
-  dependencies:
-    chalk "^2.4.1"
-    htmlparser2 "^3.10.0"
-    lodash.clonedeep "^4.5.0"
-    lodash.escaperegexp "^4.1.2"
-    lodash.isplainobject "^4.0.6"
-    lodash.isstring "^4.0.1"
-    lodash.mergewith "^4.6.1"
-    postcss "^7.0.5"
-    srcset "^1.0.0"
-    xtend "^4.0.1"
-
 "sass-loader@git://github.com/webpack-contrib/sass-loader":
   version "7.1.0"
   resolved "git://github.com/webpack-contrib/sass-loader#e279f2a129eee0bd0b624b5acd498f23a81ee35e"
@@ -7225,13 +7154,6 @@ sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
 
-srcset@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef"
-  dependencies:
-    array-uniq "^1.0.2"
-    number-is-nan "^1.0.0"
-
 sshpk@^1.7.0:
   version "1.16.0"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de"
@@ -7331,7 +7253,7 @@ string-width@^3.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
-string_decoder@^1.0.0, string_decoder@^1.1.1:
+string_decoder@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
   dependencies:
@@ -7415,7 +7337,7 @@ supports-color@^5.3.0, supports-color@^5.4.0:
   dependencies:
     has-flag "^3.0.0"
 
-supports-color@^6.0.0, supports-color@^6.1.0:
+supports-color@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
   dependencies:
@@ -7780,7 +7702,7 @@ useragent@2.3.0:
     lru-cache "4.1.x"
     tmp "0.0.x"
 
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
 
@@ -8015,10 +7937,6 @@ webpack@^4.0.0:
     watchpack "^1.5.0"
     webpack-sources "^1.3.0"
 
-whatwg-fetch@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
-
 whet.extend@~0.9.9:
   version "0.9.9"
   resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
@@ -8090,7 +8008,7 @@ xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
 
-xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
+xtend@^4.0.0, xtend@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"