diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24c193b9..ebd0e613 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,29 @@
 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
+- Removed the use of with_move parameters when fetching notifications
+
+## [2.0.3] - 2020-05-02
+### Fixed
+- Show more/less works correctly with auto-collapsed subjects and long posts
+- RTL characters won't look messed up in notifications
+
+### Changed
+- Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach:
+
+### Add
+- Follow request notification support
+
+## [2.0.2] - 2020-04-08
+### Fixed
+- Favorite/Repeat avatars not showing up on private instances/non-public posts
+- Autocorrect getting triggered in the captcha field
+- Overflow on long domains in follow/move notifications
+
+### Changed
+- Polish translation updated
 
 ## [2.0.0] - 2020-02-28
 ### Added
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 1522d4f0..34f6d6e7 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -241,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
           : federation.enabled
       })
 
+      const accountActivationRequired = metadata.accountActivationRequired
+      store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
+
       const accounts = metadata.staffAccounts
       resolveStaffAccounts({ store, accounts })
     } else {
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index aec5c39d..15a71eff 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -29,17 +29,29 @@ export default data => input => {
 export const suggestEmoji = emojis => input => {
   const noPrefix = input.toLowerCase().substr(1)
   return emojis
-    .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
+    .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
     .sort((a, b) => {
       let aScore = 0
       let bScore = 0
 
-      // Make custom emojis a priority
-      aScore += a.imageUrl ? 10 : 0
-      bScore += b.imageUrl ? 10 : 0
+      // An exact match always wins
+      aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
+      bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
 
-      // Sort alphabetically
-      const alphabetically = a.displayText > b.displayText ? 1 : -1
+      // Prioritize custom emoji a lot
+      aScore += a.imageUrl ? 100 : 0
+      bScore += b.imageUrl ? 100 : 0
+
+      // Prioritize prefix matches somewhat
+      aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+      bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+
+      // Sort by length
+      aScore -= a.displayText.length
+      bScore -= b.displayText.length
+
+      // Break ties alphabetically
+      const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
 
       return bScore - aScore + alphabetically
     })
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 37485383..e4b19d01 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -29,6 +29,11 @@ const ExtraButtons = {
       this.$store.dispatch('unmuteConversation', this.status.id)
         .then(() => this.$emit('onSuccess'))
         .catch(err => this.$emit('onError', err.error.error))
+    },
+    copyLink () {
+      navigator.clipboard.writeText(this.statusLink)
+        .then(() => this.$emit('onSuccess'))
+        .catch(err => this.$emit('onError', err.error.error))
     }
   },
   computed: {
@@ -46,6 +51,9 @@ const ExtraButtons = {
     },
     canMute () {
       return !!this.currentUser
+    },
+    statusLink () {
+      return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
     }
   }
 }
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 3a7f1283..bca93ea7 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,11 +1,13 @@
 <template>
   <Popover
-    v-if="canDelete || canMute || canPin"
     trigger="click"
     placement="top"
     class="extra-button-popover"
   >
-    <div slot="content">
+    <div
+      slot="content"
+      slot-scope="{close}"
+    >
       <div class="dropdown-menu">
         <button
           v-if="canMute && !status.thread_muted"
@@ -23,28 +25,35 @@
         </button>
         <button
           v-if="!status.pinned && canPin"
-          v-close-popover
           class="dropdown-item dropdown-item-icon"
           @click.prevent="pinStatus"
+          @click="close"
         >
           <i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
         </button>
         <button
           v-if="status.pinned && canPin"
-          v-close-popover
           class="dropdown-item dropdown-item-icon"
           @click.prevent="unpinStatus"
+          @click="close"
         >
           <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
         </button>
         <button
           v-if="canDelete"
-          v-close-popover
           class="dropdown-item dropdown-item-icon"
           @click.prevent="deleteStatus"
+          @click="close"
         >
           <i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
         </button>
+        <button
+          class="dropdown-item dropdown-item-icon"
+          @click.prevent="copyLink"
+          @click="close"
+        >
+          <i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
+        </button>
       </div>
     </div>
     <i
diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index a8931787..cbd75311 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -1,4 +1,5 @@
 import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
 
 const FollowRequestCard = {
   props: ['user'],
@@ -6,13 +7,32 @@ const FollowRequestCard = {
     BasicUserCard
   },
   methods: {
+    findFollowRequestNotificationId () {
+      const notif = notificationsFromStore(this.$store).find(
+        (notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
+      )
+      return notif && notif.id
+    },
     approveUser () {
       this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
       this.$store.dispatch('removeFollowRequest', this.user)
+
+      const notifId = this.findFollowRequestNotificationId()
+      this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
+      this.$store.dispatch('updateNotification', {
+        id: notifId,
+        updater: notification => {
+          notification.type = 'follow'
+        }
+      })
     },
     denyUser () {
+      const notifId = this.findFollowRequestNotificationId()
       this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
-      this.$store.dispatch('removeFollowRequest', this.user)
+        .then(() => {
+          this.$store.dispatch('dismissNotificationLocal', { id: notifId })
+          this.$store.dispatch('removeFollowRequest', this.user)
+        })
     }
   }
 }
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index ff1c2817..1cf4c9bc 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -2,6 +2,7 @@ import Status from '../status/status.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
 import UserCard from '../user_card/user_card.vue'
 import Timeago from '../timeago/timeago.vue'
+import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 
@@ -32,6 +33,24 @@ const Notification = {
     },
     toggleMute () {
       this.unmuted = !this.unmuted
+    },
+    approveUser () {
+      this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
+      this.$store.dispatch('removeFollowRequest', this.user)
+      this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
+      this.$store.dispatch('updateNotification', {
+        id: this.notification.id,
+        updater: notification => {
+          notification.type = 'follow'
+        }
+      })
+    },
+    denyUser () {
+      this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+        .then(() => {
+          this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
+          this.$store.dispatch('removeFollowRequest', this.user)
+        })
     }
   },
   computed: {
@@ -57,6 +76,9 @@ const Notification = {
     },
     needMute () {
       return this.$store.getters.relationship(this.user.id).muting
+    },
+    isStatusNotification () {
+      return isStatusNotification(this.notification.type)
     }
   }
 }
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 24d9fe90..0e46a2a7 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -47,7 +47,7 @@
         <span class="notification-details">
           <div class="name-and-action">
             <!-- eslint-disable vue/no-v-html -->
-            <span
+            <bdi
               v-if="!!notification.from_profile.name_html"
               class="username"
               :title="'@'+notification.from_profile.screen_name"
@@ -74,6 +74,10 @@
               <i class="fa icon-user-plus lit" />
               <small>{{ $t('notifications.followed_you') }}</small>
             </span>
+            <span v-if="notification.type === 'follow_request'">
+              <i class="fa icon-user lit" />
+              <small>{{ $t('notifications.follow_request') }}</small>
+            </span>
             <span v-if="notification.type === 'move'">
               <i class="fa icon-arrow-curved lit" />
               <small>{{ $t('notifications.migrated_to') }}</small>
@@ -87,18 +91,7 @@
             </span>
           </div>
           <div
-            v-if="notification.type === 'follow' || notification.type === 'move'"
-            class="timeago"
-          >
-            <span class="faint">
-              <Timeago
-                :time="notification.created_at"
-                :auto-update="240"
-              />
-            </span>
-          </div>
-          <div
-            v-else
+            v-if="isStatusNotification"
             class="timeago"
           >
             <router-link
@@ -112,6 +105,17 @@
               />
             </router-link>
           </div>
+          <div
+            v-else
+            class="timeago"
+          >
+            <span class="faint">
+              <Timeago
+                :time="notification.created_at"
+                :auto-update="240"
+              />
+            </span>
+          </div>
           <a
             v-if="needMute"
             href="#"
@@ -119,12 +123,30 @@
           ><i class="button-icon icon-eye-off" /></a>
         </span>
         <div
-          v-if="notification.type === 'follow'"
+          v-if="notification.type === 'follow' || notification.type === 'follow_request'"
           class="follow-text"
         >
-          <router-link :to="userProfileLink">
+          <router-link
+            :to="userProfileLink"
+            class="follow-name"
+          >
             @{{ notification.from_profile.screen_name }}
           </router-link>
+          <div
+            v-if="notification.type === 'follow_request'"
+            style="white-space: nowrap;"
+          >
+            <i
+              class="icon-ok button-icon follow-request-accept"
+              :title="$t('tool_tip.accept_follow_request')"
+              @click="approveUser()"
+            />
+            <i
+              class="icon-cancel button-icon follow-request-reject"
+              :title="$t('tool_tip.reject_follow_request')"
+              @click="denyUser()"
+            />
+          </div>
         </div>
         <div
           v-else-if="notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index a8f4430f..9efcfcf8 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -79,9 +79,38 @@
     }
   }
 
+  .follow-request-accept {
+    cursor: pointer;
+
+    &:hover {
+      color: $fallback--text;
+      color: var(--text, $fallback--text);
+    }
+  }
+
+  .follow-request-reject {
+    cursor: pointer;
+
+    &:hover {
+      color: $fallback--cRed;
+      color: var(--cRed, $fallback--cRed);
+    }
+  }
+
+
   .follow-text, .move-text {
     padding: 0.5em 0;
     overflow-wrap: break-word;
+    display: flex;
+    justify-content: space-between;
+
+    .follow-name {
+      display: block;
+      max-width: 100%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
   }
 
   .status-el {
@@ -143,6 +172,11 @@
       color: var(--cGreen, $fallback--cGreen);
     }
 
+    .icon-user.lit {
+      color: $fallback--cBlue;
+      color: var(--cBlue, $fallback--cBlue);
+    }
+
     .icon-user-plus.lit {
       color: $fallback--cBlue;
       color: var(--cBlue, $fallback--cBlue);
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 19949563..abc3bf07 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
 import { mapGetters } from 'vuex'
 
 const ReactButton = {
-  props: ['status', 'loggedIn'],
+  props: ['status'],
   data () {
     return {
       filterWord: ''
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index ab4b4fcd..0b34add1 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -37,7 +37,6 @@
       </div>
     </div>
     <i
-      v-if="loggedIn"
       slot="trigger"
       class="icon-smile button-icon add-reaction-button"
       :title="$t('tool_tip.add_reaction')"
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index ace8cc7c..dab06e1e 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,5 +1,5 @@
 import { validationMixin } from 'vuelidate'
-import { required, sameAs } from 'vuelidate/lib/validators'
+import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
 import { mapActions, mapState } from 'vuex'
 
 const registration = {
@@ -14,15 +14,17 @@ const registration = {
     },
     captcha: {}
   }),
-  validations: {
-    user: {
-      email: { required },
-      username: { required },
-      fullname: { required },
-      password: { required },
-      confirm: {
-        required,
-        sameAsPassword: sameAs('password')
+  validations () {
+    return {
+      user: {
+        email: { required: requiredIf(() => this.accountActivationRequired) },
+        username: { required },
+        fullname: { required },
+        password: { required },
+        confirm: {
+          required,
+          sameAsPassword: sameAs('password')
+        }
       }
     }
   },
@@ -43,7 +45,8 @@ const registration = {
       signedIn: (state) => !!state.users.currentUser,
       isPending: (state) => state.users.signUpPending,
       serverValidationErrors: (state) => state.users.signUpErrors,
-      termsOfService: (state) => state.instance.tos
+      termsOfService: (state) => state.instance.tos,
+      accountActivationRequired: (state) => state.instance.accountActivationRequired
     })
   },
   methods: {
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 890f4b91..9cd9d61c 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,23 +1,17 @@
-import Attachment from '../attachment/attachment.vue'
 import FavoriteButton from '../favorite_button/favorite_button.vue'
 import ReactButton from '../react_button/react_button.vue'
 import RetweetButton from '../retweet_button/retweet_button.vue'
-import Poll from '../poll/poll.vue'
 import ExtraButtons from '../extra_buttons/extra_buttons.vue'
 import PostStatusForm from '../post_status_form/post_status_form.vue'
 import UserCard from '../user_card/user_card.vue'
 import UserAvatar from '../user_avatar/user_avatar.vue'
-import Gallery from '../gallery/gallery.vue'
-import LinkPreview from '../link-preview/link-preview.vue'
 import AvatarList from '../avatar_list/avatar_list.vue'
 import Timeago from '../timeago/timeago.vue'
+import StatusContent from '../status_content/status_content.vue'
 import StatusPopover from '../status_popover/status_popover.vue'
 import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
-import fileType from 'src/services/file_type/file_type.service'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
 import { filter, unescape, uniqBy } from 'lodash'
 import { mapGetters, mapState } from 'vuex'
 
@@ -43,17 +37,10 @@ const Status = {
       replying: false,
       unmuted: false,
       userExpanded: false,
-      showingTall: this.inConversation && this.focused,
-      showingLongSubject: false,
-      error: null,
-      // not as computed because it sets the initial state which will be changed later
-      expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+      error: null
     }
   },
   computed: {
-    localCollapseSubjectDefault () {
-      return this.mergedConfig.collapseMessageWithSubject
-    },
     muteWords () {
       return this.mergedConfig.muteWords
     },
@@ -79,10 +66,6 @@ const Status = {
       const highlight = this.mergedConfig.highlight
       return highlightStyle(highlight[user.screen_name])
     },
-    hideAttachments () {
-      return (this.mergedConfig.hideAttachments && !this.inConversation) ||
-        (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
-    },
     userProfileLink () {
       return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
     },
@@ -141,20 +124,6 @@ const Status = {
       // use conversation highlight only when in conversation
       return this.status.id === this.highlight
     },
-    // This is a bit hacky, but we want to approximate post height before rendering
-    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
-    // as well as approximate line count by counting characters and approximating ~80
-    // per line.
-    //
-    // Using max-height + overflow: auto for status components resulted in false positives
-    // very often with japanese characters, and it was very annoying.
-    tallStatus () {
-      const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
-      return lengthScore > 20
-    },
-    longSubject () {
-      return this.status.summary.length > 900
-    },
     isReply () {
       return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
     },
@@ -197,33 +166,6 @@ const Status = {
       }
       return this.status.attentions.length > 0
     },
-    hideSubjectStatus () {
-      if (this.tallStatus && !this.localCollapseSubjectDefault) {
-        return false
-      }
-      return !this.expandingSubject && this.status.summary
-    },
-    hideTallStatus () {
-      if (this.status.summary && this.localCollapseSubjectDefault) {
-        return false
-      }
-      if (this.showingTall) {
-        return false
-      }
-      return this.tallStatus
-    },
-    showingMore () {
-      return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
-    },
-    nsfwClickthrough () {
-      if (!this.status.nsfw) {
-        return false
-      }
-      if (this.status.summary && this.localCollapseSubjectDefault) {
-        return false
-      }
-      return true
-    },
     replySubject () {
       if (!this.status.summary) return ''
       const decodedSummary = unescape(this.status.summary)
@@ -237,83 +179,6 @@ const Status = {
         return ''
       }
     },
-    attachmentSize () {
-      if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
-        (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
-        (this.status.attachments.length > this.maxThumbnails)) {
-        return 'hide'
-      } else if (this.compact) {
-        return 'small'
-      }
-      return 'normal'
-    },
-    galleryTypes () {
-      if (this.attachmentSize === 'hide') {
-        return []
-      }
-      return this.mergedConfig.playVideosInModal
-        ? ['image', 'video']
-        : ['image']
-    },
-    galleryAttachments () {
-      return this.status.attachments.filter(
-        file => fileType.fileMatchesSomeType(this.galleryTypes, file)
-      )
-    },
-    nonGalleryAttachments () {
-      return this.status.attachments.filter(
-        file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
-      )
-    },
-    hasImageAttachments () {
-      return this.status.attachments.some(
-        file => fileType.fileType(file.mimetype) === 'image'
-      )
-    },
-    hasVideoAttachments () {
-      return this.status.attachments.some(
-        file => fileType.fileType(file.mimetype) === 'video'
-      )
-    },
-    maxThumbnails () {
-      return this.mergedConfig.maxThumbnails
-    },
-    postBodyHtml () {
-      const html = this.status.statusnet_html
-
-      if (this.mergedConfig.greentext) {
-        try {
-          if (html.includes('&gt;')) {
-            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
-            return processHtml(html, (string) => {
-              if (string.includes('&gt;') &&
-                  string
-                    .replace(/<[^>]+?>/gi, '') // remove all tags
-                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
-                    .trim()
-                    .startsWith('&gt;')) {
-                return `<span class='greentext'>${string}</span>`
-              } else {
-                return string
-              }
-            })
-          } else {
-            return html
-          }
-        } catch (e) {
-          console.err('Failed to process status html', e)
-          return html
-        }
-      } else {
-        return html
-      }
-    },
-    contentHtml () {
-      if (!this.status.summary_html) {
-        return this.postBodyHtml
-      }
-      return this.status.summary_html + '<br />' + this.postBodyHtml
-    },
     combinedFavsAndRepeatsUsers () {
       // Use the status from the global status repository since favs and repeats are saved in it
       const combinedUsers = [].concat(
@@ -322,9 +187,6 @@ const Status = {
       )
       return uniqBy(combinedUsers, 'id')
     },
-    ownStatus () {
-      return this.status.user.id === this.currentUser.id
-    },
     tags () {
       return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
     },
@@ -338,21 +200,18 @@ const Status = {
     })
   },
   components: {
-    Attachment,
     FavoriteButton,
     ReactButton,
     RetweetButton,
     ExtraButtons,
     PostStatusForm,
-    Poll,
     UserCard,
     UserAvatar,
-    Gallery,
-    LinkPreview,
     AvatarList,
     Timeago,
     StatusPopover,
-    EmojiReactions
+    EmojiReactions,
+    StatusContent
   },
   methods: {
     visibilityIcon (visibility) {
@@ -373,32 +232,6 @@ const Status = {
     clearError () {
       this.error = undefined
     },
-    linkClicked (event) {
-      const target = event.target.closest('.status-content a')
-      if (target) {
-        if (target.className.match(/mention/)) {
-          const href = target.href
-          const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
-          if (attn) {
-            event.stopPropagation()
-            event.preventDefault()
-            const link = this.generateUserProfileLink(attn.id, attn.screen_name)
-            this.$router.push(link)
-            return
-          }
-        }
-        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
-          // Extract tag name from link url
-          const tag = extractTagFromUrl(target.href)
-          if (tag) {
-            const link = this.generateTagLink(tag)
-            this.$router.push(link)
-            return
-          }
-        }
-        window.open(target.href, '_blank')
-      }
-    },
     toggleReplying () {
       this.replying = !this.replying
     },
@@ -416,26 +249,8 @@ const Status = {
     toggleUserExpanded () {
       this.userExpanded = !this.userExpanded
     },
-    toggleShowMore () {
-      if (this.showingTall) {
-        this.showingTall = false
-      } else if (this.expandingSubject && this.status.summary) {
-        this.expandingSubject = false
-      } else if (this.hideTallStatus) {
-        this.showingTall = true
-      } else if (this.hideSubjectStatus && this.status.summary) {
-        this.expandingSubject = true
-      }
-    },
     generateUserProfileLink (id, name) {
       return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
-    },
-    generateTagLink (tag) {
-      return `/tag/${tag}`
-    },
-    setMedia () {
-      const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
-      return () => this.$store.dispatch('setMedia', attachments)
     }
   },
   watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index dd7cd579..e4c7545b 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -226,118 +226,12 @@
             </div>
           </div>
 
-          <div
-            v-if="longSubject"
-            class="status-content-wrapper"
-            :class="{ 'tall-status': !showingLongSubject }"
-          >
-            <a
-              v-if="!showingLongSubject"
-              class="tall-status-hider"
-              :class="{ 'tall-status-hider_focused': isFocused }"
-              href="#"
-              @click.prevent="showingLongSubject=true"
-            >{{ $t("general.show_more") }}</a>
-            <div
-              class="status-content media-body"
-              @click.prevent="linkClicked"
-              v-html="contentHtml"
-            />
-            <a
-              v-if="showingLongSubject"
-              href="#"
-              class="status-unhider"
-              @click.prevent="showingLongSubject=false"
-            >{{ $t("general.show_less") }}</a>
-          </div>
-          <div
-            v-else
-            :class="{'tall-status': hideTallStatus}"
-            class="status-content-wrapper"
-          >
-            <a
-              v-if="hideTallStatus"
-              class="tall-status-hider"
-              :class="{ 'tall-status-hider_focused': isFocused }"
-              href="#"
-              @click.prevent="toggleShowMore"
-            >{{ $t("general.show_more") }}</a>
-            <div
-              v-if="!hideSubjectStatus"
-              class="status-content media-body"
-              @click.prevent="linkClicked"
-              v-html="contentHtml"
-            />
-            <div
-              v-else
-              class="status-content media-body"
-              @click.prevent="linkClicked"
-              v-html="status.summary_html"
-            />
-            <a
-              v-if="hideSubjectStatus"
-              href="#"
-              class="cw-status-hider"
-              @click.prevent="toggleShowMore"
-            >
-              {{ $t("general.show_more") }}
-              <span
-                v-if="hasImageAttachments"
-                class="icon-picture"
-              />
-              <span
-                v-if="hasVideoAttachments"
-                class="icon-video"
-              />
-              <span
-                v-if="status.card"
-                class="icon-link"
-              />
-            </a>
-            <a
-              v-if="showingMore"
-              href="#"
-              class="status-unhider"
-              @click.prevent="toggleShowMore"
-            >{{ $t("general.show_less") }}</a>
-          </div>
-
-          <div v-if="status.poll && status.poll.options">
-            <poll :base-poll="status.poll" />
-          </div>
-
-          <div
-            v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
-            class="attachments media-body"
-          >
-            <attachment
-              v-for="attachment in nonGalleryAttachments"
-              :key="attachment.id"
-              class="non-gallery"
-              :size="attachmentSize"
-              :nsfw="nsfwClickthrough"
-              :attachment="attachment"
-              :allow-play="true"
-              :set-media="setMedia()"
-            />
-            <gallery
-              v-if="galleryAttachments.length > 0"
-              :nsfw="nsfwClickthrough"
-              :attachments="galleryAttachments"
-              :set-media="setMedia()"
-            />
-          </div>
-
-          <div
-            v-if="status.card && !hideSubjectStatus && !noHeading"
-            class="link-preview media-body"
-          >
-            <link-preview
-              :card="status.card"
-              :size="attachmentSize"
-              :nsfw="nsfwClickthrough"
-            />
-          </div>
+          <StatusContent
+            :status="status"
+            :no-heading="noHeading"
+            :highlight="highlight"
+            :focused="isFocused"
+          />
 
           <transition name="fade">
             <div
@@ -404,7 +298,7 @@
               :status="status"
             />
             <ReactButton
-              :logged-in="loggedIn"
+              v-if="loggedIn"
               :status="status"
             />
             <extra-buttons
@@ -630,105 +524,6 @@ $status-margin: 0.75em;
     }
   }
 
-  .tall-status {
-    position: relative;
-    height: 220px;
-    overflow-x: hidden;
-    overflow-y: hidden;
-    z-index: 1;
-    .status-content {
-      height: 100%;
-      mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
-            linear-gradient(to top, white, white);
-      /* Autoprefixed seem to ignore this one, and also syntax is different */
-      -webkit-mask-composite: xor;
-      mask-composite: exclude;
-    }
-  }
-
-  .tall-status-hider {
-    display: inline-block;
-    word-break: break-all;
-    position: absolute;
-    height: 70px;
-    margin-top: 150px;
-    width: 100%;
-    text-align: center;
-    line-height: 110px;
-    z-index: 2;
-  }
-
-  .status-unhider, .cw-status-hider {
-    width: 100%;
-    text-align: center;
-    display: inline-block;
-    word-break: break-all;
-  }
-
-  .status-content {
-    font-family: var(--postFont, sans-serif);
-    line-height: 1.4em;
-    white-space: pre-wrap;
-
-    a {
-      color: $fallback--link;
-      color: var(--postLink, $fallback--link);
-    }
-
-    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;
-    }
-
-    pre {
-      overflow: auto;
-    }
-
-    code, samp, kbd, var, pre {
-      font-family: var(--postCodeFont, monospace);
-    }
-
-    p {
-      margin: 0 0 1em 0;
-    }
-
-    p:last-child {
-      margin: 0 0 0 0;
-    }
-
-    h1 {
-      font-size: 1.1em;
-      line-height: 1.2em;
-      margin: 1.4em 0;
-    }
-
-    h2 {
-      font-size: 1.1em;
-      margin: 1.0em 0;
-    }
-
-    h3 {
-      font-size: 1em;
-      margin: 1.2em 0;
-    }
-
-    h4 {
-      margin: 1.1em 0;
-    }
-  }
-
   .retweet-info {
     padding: 0.4em $status-margin;
     margin: 0;
@@ -790,11 +585,6 @@ $status-margin: 0.75em;
   }
 }
 
-.greentext {
-  color: $fallback--cGreen;
-  color: var(--cGreen, $fallback--cGreen);
-}
-
 .status-conversation {
   border-left-style: solid;
 }
@@ -866,14 +656,6 @@ a.unmute {
   flex: 1;
 }
 
-.timeline :not(.panel-disabled) > {
-  .status-el:last-child {
-    border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
-    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
-    border-bottom: none;
-  }
-}
-
 .favs-repeated-users {
   margin-top: $status-margin;
 
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
new file mode 100644
index 00000000..ccc01b6f
--- /dev/null
+++ b/src/components/status_content/status_content.js
@@ -0,0 +1,210 @@
+import Attachment from '../attachment/attachment.vue'
+import Poll from '../poll/poll.vue'
+import Gallery from '../gallery/gallery.vue'
+import LinkPreview from '../link-preview/link-preview.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import fileType from 'src/services/file_type/file_type.service'
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { mapGetters, mapState } from 'vuex'
+
+const StatusContent = {
+  name: 'StatusContent',
+  props: [
+    'status',
+    'focused',
+    'noHeading',
+    'fullContent'
+  ],
+  data () {
+    return {
+      showingTall: this.inConversation && this.focused,
+      showingLongSubject: false,
+      // not as computed because it sets the initial state which will be changed later
+      expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+    }
+  },
+  computed: {
+    localCollapseSubjectDefault () {
+      return this.mergedConfig.collapseMessageWithSubject
+    },
+    hideAttachments () {
+      return (this.mergedConfig.hideAttachments && !this.inConversation) ||
+        (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
+    },
+    // This is a bit hacky, but we want to approximate post height before rendering
+    // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
+    // as well as approximate line count by counting characters and approximating ~80
+    // per line.
+    //
+    // Using max-height + overflow: auto for status components resulted in false positives
+    // very often with japanese characters, and it was very annoying.
+    tallStatus () {
+      const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+      return lengthScore > 20
+    },
+    longSubject () {
+      return this.status.summary.length > 900
+    },
+    // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
+    mightHideBecauseSubject () {
+      return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
+    },
+    mightHideBecauseTall () {
+      return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+    },
+    hideSubjectStatus () {
+      return this.mightHideBecauseSubject && !this.expandingSubject
+    },
+    hideTallStatus () {
+      return this.mightHideBecauseTall && !this.showingTall
+    },
+    showingMore () {
+      return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
+    },
+    nsfwClickthrough () {
+      if (!this.status.nsfw) {
+        return false
+      }
+      if (this.status.summary && this.localCollapseSubjectDefault) {
+        return false
+      }
+      return true
+    },
+    attachmentSize () {
+      if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
+        (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
+        (this.status.attachments.length > this.maxThumbnails)) {
+        return 'hide'
+      } else if (this.compact) {
+        return 'small'
+      }
+      return 'normal'
+    },
+    galleryTypes () {
+      if (this.attachmentSize === 'hide') {
+        return []
+      }
+      return this.mergedConfig.playVideosInModal
+        ? ['image', 'video']
+        : ['image']
+    },
+    galleryAttachments () {
+      return this.status.attachments.filter(
+        file => fileType.fileMatchesSomeType(this.galleryTypes, file)
+      )
+    },
+    nonGalleryAttachments () {
+      return this.status.attachments.filter(
+        file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
+      )
+    },
+    hasImageAttachments () {
+      return this.status.attachments.some(
+        file => fileType.fileType(file.mimetype) === 'image'
+      )
+    },
+    hasVideoAttachments () {
+      return this.status.attachments.some(
+        file => fileType.fileType(file.mimetype) === 'video'
+      )
+    },
+    maxThumbnails () {
+      return this.mergedConfig.maxThumbnails
+    },
+    postBodyHtml () {
+      const html = this.status.statusnet_html
+
+      if (this.mergedConfig.greentext) {
+        try {
+          if (html.includes('&gt;')) {
+            // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+            return processHtml(html, (string) => {
+              if (string.includes('&gt;') &&
+                  string
+                    .replace(/<[^>]+?>/gi, '') // remove all tags
+                    .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+                    .trim()
+                    .startsWith('&gt;')) {
+                return `<span class='greentext'>${string}</span>`
+              } else {
+                return string
+              }
+            })
+          } else {
+            return html
+          }
+        } catch (e) {
+          console.err('Failed to process status html', e)
+          return html
+        }
+      } else {
+        return html
+      }
+    },
+    contentHtml () {
+      if (!this.status.summary_html) {
+        return this.postBodyHtml
+      }
+      return this.status.summary_html + '<br />' + this.postBodyHtml
+    },
+    ...mapGetters(['mergedConfig']),
+    ...mapState({
+      betterShadow: state => state.interface.browserSupport.cssFilter,
+      currentUser: state => state.users.currentUser
+    })
+  },
+  components: {
+    Attachment,
+    Poll,
+    Gallery,
+    LinkPreview
+  },
+  methods: {
+    linkClicked (event) {
+      const target = event.target.closest('.status-content a')
+      if (target) {
+        if (target.className.match(/mention/)) {
+          const href = target.href
+          const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
+          if (attn) {
+            event.stopPropagation()
+            event.preventDefault()
+            const link = this.generateUserProfileLink(attn.id, attn.screen_name)
+            this.$router.push(link)
+            return
+          }
+        }
+        if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
+          // Extract tag name from link url
+          const tag = extractTagFromUrl(target.href)
+          if (tag) {
+            const link = this.generateTagLink(tag)
+            this.$router.push(link)
+            return
+          }
+        }
+        window.open(target.href, '_blank')
+      }
+    },
+    toggleShowMore () {
+      if (this.mightHideBecauseTall) {
+        this.showingTall = !this.showingTall
+      } else if (this.mightHideBecauseSubject) {
+        this.expandingSubject = !this.expandingSubject
+      }
+    },
+    generateUserProfileLink (id, name) {
+      return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
+    },
+    generateTagLink (tag) {
+      return `/tag/${tag}`
+    },
+    setMedia () {
+      const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+      return () => this.$store.dispatch('setMedia', attachments)
+    }
+  }
+}
+
+export default StatusContent
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
new file mode 100644
index 00000000..8c2e8749
--- /dev/null
+++ b/src/components/status_content/status_content.vue
@@ -0,0 +1,240 @@
+<template>
+  <!-- eslint-disable vue/no-v-html -->
+  <div class="status-body">
+    <slot name="header" />
+    <div
+      v-if="longSubject"
+      class="status-content-wrapper"
+      :class="{ 'tall-status': !showingLongSubject }"
+    >
+      <a
+        v-if="!showingLongSubject"
+        class="tall-status-hider"
+        :class="{ 'tall-status-hider_focused': focused }"
+        href="#"
+        @click.prevent="showingLongSubject=true"
+      >
+        {{ $t("general.show_more") }}
+        <span
+          v-if="hasImageAttachments"
+          class="icon-picture"
+        />
+        <span
+          v-if="hasVideoAttachments"
+          class="icon-video"
+        />
+        <span
+          v-if="status.card"
+          class="icon-link"
+        />
+      </a>
+      <div
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="contentHtml"
+      />
+      <a
+        v-if="showingLongSubject"
+        href="#"
+        class="status-unhider"
+        @click.prevent="showingLongSubject=false"
+      >{{ $t("general.show_less") }}</a>
+    </div>
+    <div
+      v-else
+      :class="{'tall-status': hideTallStatus}"
+      class="status-content-wrapper"
+    >
+      <a
+        v-if="hideTallStatus"
+        class="tall-status-hider"
+        :class="{ 'tall-status-hider_focused': focused }"
+        href="#"
+        @click.prevent="toggleShowMore"
+      >{{ $t("general.show_more") }}</a>
+      <div
+        v-if="!hideSubjectStatus"
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="contentHtml"
+      />
+      <div
+        v-else
+        class="status-content media-body"
+        @click.prevent="linkClicked"
+        v-html="status.summary_html"
+      />
+      <a
+        v-if="hideSubjectStatus"
+        href="#"
+        class="cw-status-hider"
+        @click.prevent="toggleShowMore"
+      >{{ $t("general.show_more") }}</a>
+      <a
+        v-if="showingMore"
+        href="#"
+        class="status-unhider"
+        @click.prevent="toggleShowMore"
+      >{{ $t("general.show_less") }}</a>
+    </div>
+
+    <div v-if="status.poll && status.poll.options">
+      <poll :base-poll="status.poll" />
+    </div>
+
+    <div
+      v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
+      class="attachments media-body"
+    >
+      <attachment
+        v-for="attachment in nonGalleryAttachments"
+        :key="attachment.id"
+        class="non-gallery"
+        :size="attachmentSize"
+        :nsfw="nsfwClickthrough"
+        :attachment="attachment"
+        :allow-play="true"
+        :set-media="setMedia()"
+      />
+      <gallery
+        v-if="galleryAttachments.length > 0"
+        :nsfw="nsfwClickthrough"
+        :attachments="galleryAttachments"
+        :set-media="setMedia()"
+      />
+    </div>
+
+    <div
+      v-if="status.card && !hideSubjectStatus && !noHeading"
+      class="link-preview media-body"
+    >
+      <link-preview
+        :card="status.card"
+        :size="attachmentSize"
+        :nsfw="nsfwClickthrough"
+      />
+    </div>
+    <slot name="footer" />
+  </div>
+  <!-- eslint-enable vue/no-v-html -->
+</template>
+
+<script src="./status_content.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+$status-margin: 0.75em;
+
+.status-body {
+  flex: 1;
+  min-width: 0;
+
+  .tall-status {
+    position: relative;
+    height: 220px;
+    overflow-x: hidden;
+    overflow-y: hidden;
+    z-index: 1;
+    .status-content {
+      height: 100%;
+      mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+            linear-gradient(to top, white, white);
+      /* Autoprefixed seem to ignore this one, and also syntax is different */
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+    }
+  }
+
+  .tall-status-hider {
+    display: inline-block;
+    word-break: break-all;
+    position: absolute;
+    height: 70px;
+    margin-top: 150px;
+    width: 100%;
+    text-align: center;
+    line-height: 110px;
+    z-index: 2;
+  }
+
+  .status-unhider, .cw-status-hider {
+    width: 100%;
+    text-align: center;
+    display: inline-block;
+    word-break: break-all;
+  }
+
+  .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;
+    }
+
+    pre {
+      overflow: auto;
+    }
+
+    code, samp, kbd, var, pre {
+      font-family: var(--postCodeFont, monospace);
+    }
+
+    p {
+      margin: 0 0 1em 0;
+    }
+
+    p:last-child {
+      margin: 0 0 0 0;
+    }
+
+    h1 {
+      font-size: 1.1em;
+      line-height: 1.2em;
+      margin: 1.4em 0;
+    }
+
+    h2 {
+      font-size: 1.1em;
+      margin: 1.0em 0;
+    }
+
+    h3 {
+      font-size: 1em;
+      margin: 1.2em 0;
+    }
+
+    h4 {
+      margin: 1.1em 0;
+    }
+  }
+}
+
+.greentext {
+  color: $fallback--cGreen;
+  color: var(--cGreen, $fallback--cGreen);
+}
+
+.timeline :not(.panel-disabled) > {
+  .status-el:last-child {
+    border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+    border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+    border-bottom: none;
+  }
+}
+
+</style>
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8b2336b4..ad184520 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -379,6 +379,7 @@
           :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">
@@ -404,6 +405,17 @@
                 </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
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 54d0608e..d5748719 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -124,6 +124,7 @@
     "broken_favorite": "Unknown status, searching for it...",
     "favorited_you": "favorited your status",
     "followed_you": "followed you",
+    "follow_request": "wants to follow you",
     "load_older": "Load older notifications",
     "notifications": "Notifications",
     "read": "Read!",
@@ -404,11 +405,14 @@
     "fun": "Fun",
     "greentext": "Meme arrows",
     "notifications": "Notifications",
+    "notification_setting_filters": "Filters",
     "notification_setting": "Receive notifications from:",
     "notification_setting_follows": "Users you follow",
     "notification_setting_non_follows": "Users you do not follow",
     "notification_setting_followers": "Users who follow you",
     "notification_setting_non_followers": "Users who do not follow you",
+    "notification_setting_privacy": "Privacy",
+    "notification_setting_privacy_option": "Hide the sender and contents of push notifications",
     "notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
     "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
     "enable_web_push_notifications": "Enable web push notifications",
@@ -616,7 +620,8 @@
     "replies_list": "Replies:",
     "mute_conversation": "Mute conversation",
     "unmute_conversation": "Unmute conversation",
-    "status_unavailable": "Status unavailable"
+    "status_unavailable": "Status unavailable",
+    "copy_link": "Copy link to status"
   },
   "user_card": {
     "approve": "Approve",
@@ -697,7 +702,9 @@
     "reply": "Reply",
     "favorite": "Favorite",
     "add_reaction": "Add Reaction",
-    "user_settings": "User Settings"
+    "user_settings": "User Settings",
+    "accept_follow_request": "Accept follow request",
+    "reject_follow_request": "Reject follow request"
   },
   "upload":{
     "error": {
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 51cadfb6..4a4b1e31 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -1,7 +1,47 @@
 {
+  "about": {
+    "mrf": {
+      "federation": "Federacja",
+      "keyword": {
+        "keyword_policies": "Zasady słów kluczowych",
+        "ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
+        "reject": "Odrzucanie",
+        "replace": "Zastąpienie",
+        "is_replaced_by": "→"
+      },
+      "mrf_policies": "Włączone zasady MRF",
+      "mrf_policies_desc": "Zasady MRF zmieniają zachowanie federowania instancji. Następujące zasady są włączone:",
+      "simple": {
+        "simple_policies": "Zasady specyficzne dla instancji",
+        "accept": "Akceptowanie",
+        "accept_desc": "Ta instancja akceptuje tylko posty z wymienionych instancji:",
+        "reject": "Odrzucanie",
+        "reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:",
+        "quarantine": "Kwarantanna",
+        "quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:",
+        "ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
+        "ftl_removal_desc": "Ta instancja usuwa te instancje z \"Całej znanej sieci\"",
+        "media_removal": "Usuwanie multimediów",
+        "media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:",
+        "media_nsfw": "Multimedia ustawione jako wrażliwe",
+        "media_nsfw_desc": "Ta instancja wymusza, by multimedia z wymienionych instancji były ustawione jako wrażliwe:"
+      }
+    },
+    "staff": "Obsługa"
+  },
   "chat": {
     "title": "Czat"
   },
+  "domain_mute_card": {
+    "mute": "Wycisz",
+    "mute_progress": "Wyciszam...",
+    "unmute": "Odcisz",
+    "unmute_progress": "Odciszam..."
+  },
+  "exporter": {
+    "export": "Eksportuj",
+    "processing": "Przetwarzam, za chwilę zostaniesz zapytany o ściągnięcie pliku"
+  },
   "features_panel": {
     "chat": "Czat",
     "gopher": "Gopher",
@@ -20,7 +60,15 @@
     "submit": "Wyślij",
     "more": "Więcej",
     "generic_error": "Wystąpił błąd",
-    "optional": "nieobowiązkowe"
+    "optional": "nieobowiązkowe",
+    "show_more": "Pokaż więcej",
+    "show_less": "Pokaż mniej",
+    "dismiss": "Odrzuć",
+    "cancel": "Anuluj",
+    "disable": "Wyłącz",
+    "enable": "Włącz",
+    "confirm": "Potwierdź",
+    "verify": "Zweryfikuj"
   },
   "image_cropper": {
     "crop_picture": "Przytnij obrazek",
@@ -28,6 +76,11 @@
     "save_without_cropping": "Zapisz bez przycinania",
     "cancel": "Anuluj"
   },
+  "importer": {
+    "submit": "Wyślij",
+    "success": "Zaimportowano pomyślnie",
+    "error": "Wystąpił błąd podczas importowania pliku."
+  },
   "login": {
     "login": "Zaloguj",
     "description": "Zaloguj używając OAuth",
@@ -36,7 +89,15 @@
     "placeholder": "n.p. lain",
     "register": "Zarejestruj",
     "username": "Użytkownik",
-    "hint": "Zaloguj się, aby dołączyć do dyskusji"
+    "hint": "Zaloguj się, aby dołączyć do dyskusji",
+    "authentication_code": "Kod weryfikacyjny",
+    "enter_recovery_code": "Wprowadź kod zapasowy",
+    "enter_two_factor_code": "Wprowadź kod weryfikacyjny",
+    "recovery_code": "Kod zapasowy",
+    "heading" : {
+      "totp" : "Weryfikacja dwuetapowa",
+      "recovery" : "Zapasowa weryfikacja dwuetapowa"
+    }
   },
   "media_modal": {
     "previous": "Poprzednie",
@@ -44,15 +105,18 @@
   },
   "nav": {
     "about": "O nas",
+    "administration": "Administracja",
     "back": "Wróć",
     "chat": "Lokalny czat",
     "friend_requests": "Prośby o możliwość obserwacji",
     "mentions": "Wzmianki",
+    "interactions": "Interakcje",
     "dms": "Wiadomości prywatne",
     "public_tl": "Publiczna oś czasu",
     "timeline": "Oś czasu",
     "twkn": "Cała znana sieć",
     "user_search": "Wyszukiwanie użytkowników",
+    "search": "Wyszukiwanie",
     "who_to_follow": "Sugestie obserwacji",
     "preferences": "Preferencje"
   },
@@ -64,7 +128,40 @@
     "notifications": "Powiadomienia",
     "read": "Przeczytane!",
     "repeated_you": "powtórzył(-a) twój status",
-    "no_more_notifications": "Nie masz więcej powiadomień"
+    "no_more_notifications": "Nie masz więcej powiadomień",
+    "migrated_to": "wyemigrował do",
+    "reacted_with": "zareagował z {0}"
+  },
+  "polls": {
+    "add_poll": "Dodaj ankietę",
+    "add_option": "Dodaj opcję",
+    "option": "Opcja",
+    "votes": "głosów",
+    "vote": "Głosuj",
+    "type": "Typ ankiety",
+    "single_choice": "jednokrotnego wyboru",
+    "multiple_choices": "wielokrotnego wyboru",
+    "expiry": "Czas trwania ankiety",
+    "expires_in": "Ankieta kończy się za{0}",
+    "expired": "Ankieta skończyła się {0} temu",
+    "not_enough_options": "Zbyt mało unikalnych opcji w ankiecie"
+  },
+  "emoji": {
+    "stickers": "Naklejki",
+    "emoji": "Emoji",
+    "keep_open": "Zostaw selektor otwarty",
+    "search_emoji": "Wyszukaj emoji",
+    "add_emoji": "Wstaw emoji",
+    "custom": "Niestandardowe emoji",
+    "unicode": "Emoji unicode",
+    "load_all_hint": "Załadowano pierwsze {saneAmount} emoji, Załadowanie wszystkich emoji może spowodować problemy z wydajnością.",
+    "load_all": "Ładuję wszystkie {emojiAmount} emoji"
+  },
+  "interactions": {
+    "favs_repeats": "Powtórzenia i ulubione",
+    "follows": "Nowi obserwujący",
+    "moves": "Użytkownik migruje",
+    "load_older": "Załaduj starsze interakcje"
   },
   "post_status": {
     "new_status": "Dodaj nowy status",
@@ -79,8 +176,14 @@
     },
     "content_warning": "Temat (nieobowiązkowy)",
     "default": "Właśnie wróciłem z kościoła",
-    "direct_warning": "Ten wpis zobaczą tylko osoby, o których wspomniałeś(-aś).",
+    "direct_warning_to_all": "Ten wpis zobaczą wszystkie osoby, o których wspomniałeś(-aś).",
+    "direct_warning_to_first_only": "Ten wpis zobaczą tylko te osoby, o których wspomniałeś(-aś) na początku wiadomości.",
     "posting": "Wysyłanie",
+    "scope_notice": {
+      "public": "Ten post będzie widoczny dla każdego",
+      "private": "Ten post będzie widoczny tylko dla twoich obserwujących",
+      "unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci"
+    },
     "scope": {
       "direct": "Bezpośredni – Tylko dla wspomnianych użytkowników",
       "private": "Tylko dla obserwujących – Umieść dla osób, które cię obserwują",
@@ -109,8 +212,40 @@
       "password_confirmation_match": "musi być takie jak hasło"
     }
   },
+  "remote_user_resolver": {
+    "remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych",
+    "searching_for": "Szukam",
+    "error": "Nie znaleziono."
+  },
+  "selectable_list": {
+    "select_all": "Zaznacz wszystko"
+  },
   "settings": {
     "app_name": "Nazwa aplikacji",
+    "security": "Bezpieczeństwo",
+    "enter_current_password_to_confirm": "Wprowadź obecne hasło, by potwierdzić twoją tożsamość",
+    "mfa": {
+      "otp" : "OTP",
+      "setup_otp" : "Ustaw OTP",
+      "wait_pre_setup_otp" : "początkowe ustawianie OTP",
+      "confirm_and_enable" : "Potwierdź i włącz OTP",
+      "title": "Weryfikacja dwuetapowa",
+      "generate_new_recovery_codes" : "Wygeneruj nowe kody zapasowe",
+      "warning_of_generate_new_codes" : "Po tym gdy generujesz nowe kody zapasowe, stare przestaną działać.",
+      "recovery_codes" : "Kody zapasowe.",
+      "waiting_a_recovery_codes": "Otrzymuję kody zapasowe...",
+      "recovery_codes_warning" : "Spisz kody na kartce papieru, albo zapisz je w bezpiecznym miejscu - inaczej nie zobaczysz ich już nigdy. Jeśli stracisz dostęp do twojej aplikacji 2FA i kodów zapasowych, nie będziesz miał dostępu do swojego konta.",
+      "authentication_methods" : "Metody weryfikacji",
+      "scan": {
+        "title": "Skanuj",
+        "desc": "Zeskanuj ten kod QR używając twojej aplikacji 2FA albo wpisz ten klucz:",
+        "secret_code": "Klucz"
+      },
+      "verify": {
+        "desc": "By włączyć weryfikację dwuetapową, wpisz kod z twojej aplikacji 2FA:"
+      }
+    },
+    "allow_following_move": "Zezwalaj na automatyczną obserwację gdy obserwowane konto migruje",
     "attachmentRadius": "Załączniki",
     "attachments": "Załączniki",
     "autoload": "Włącz automatyczne ładowanie po przewinięciu do końca strony",
@@ -119,12 +254,20 @@
     "avatarRadius": "Awatary",
     "background": "Tło",
     "bio": "Bio",
+    "block_export": "Eksport blokad",
+    "block_export_button": "Eksportuj twoje blokady do pliku .csv",
+    "block_import": "Import blokad",
+    "block_import_error": "Wystąpił błąd podczas importowania blokad",
+    "blocks_imported": "Zaimportowano blokady, przetwarzanie może zająć trochę czasu.",
     "blocks_tab": "Bloki",
     "btnRadius": "Przyciski",
     "cBlue": "Niebieski (odpowiedz, obserwuj)",
     "cGreen": "Zielony (powtórzenia)",
     "cOrange": "Pomarańczowy (ulubione)",
     "cRed": "Czerwony (anuluj)",
+    "change_email": "Zmień email",
+    "change_email_error": "Wystąpił problem podczas zmiany emaila.",
+    "changed_email": "Pomyślnie zmieniono email!",
     "change_password": "Zmień hasło",
     "change_password_error": "Podczas zmiany hasła wystąpił problem.",
     "changed_password": "Pomyślnie zmieniono hasło!",
@@ -140,16 +283,20 @@
     "delete_account_description": "Trwale usuń konto i wszystkie posty.",
     "delete_account_error": "Wystąpił problem z usuwaniem twojego konta. Jeżeli problem powtarza się, poinformuj administratora swojej instancji.",
     "delete_account_instructions": "Wprowadź swoje hasło w poniższe pole aby potwierdzić usunięcie konta.",
+    "discoverable": "Zezwól na odkrywanie tego konta w wynikach wyszukiwania i innych usługa.",
+    "domain_mutes": "Domeny",
     "avatar_size_instruction": "Zalecany minimalny rozmiar awatarów to 150x150 pikseli.",
+    "pad_emoji": "Dodaj odstęp z obu stron emoji podczas dodawania selektorem",
+    "emoji_reactions_on_timeline": "Pokaż reakcje emoji na osi czasu",
     "export_theme": "Zapisz motyw",
     "filtering": "Filtrowanie",
     "filtering_explanation": "Wszystkie statusy zawierające te słowa będą wyciszone. Jedno słowo na linijkę.",
     "follow_export": "Eksport obserwowanych",
     "follow_export_button": "Eksportuj swoją listę obserwowanych do pliku CSV",
-    "follow_export_processing": "Przetwarzanie, wkrótce twój plik zacznie się ściągać.",
     "follow_import": "Import obserwowanych",
     "follow_import_error": "Błąd przy importowaniu obserwowanych",
     "follows_imported": "Obserwowani zaimportowani! Przetwarzanie może trochę potrwać.",
+    "accent": "Akcent",
     "foreground": "Pierwszy plan",
     "general": "Ogólne",
     "hide_attachments_in_convo": "Ukrywaj załączniki w rozmowach",
@@ -162,6 +309,7 @@
     "hide_post_stats": "Ukrywaj statysyki postów (np. liczbę polubień)",
     "hide_user_stats": "Ukrywaj statysyki użytkowników (np. liczbę obserwujących)",
     "hide_filtered_statuses": "Ukrywaj filtrowane statusy",
+    "import_blocks_from_a_csv_file": "Importuj blokady z pliku CSV",
     "import_followers_from_a_csv_file": "Importuj obserwowanych z pliku CSV",
     "import_theme": "Załaduj motyw",
     "inputRadius": "Pola tekstowe",
@@ -181,17 +329,22 @@
     "use_contain_fit": "Nie przycinaj załączników na miniaturach",
     "name": "Imię",
     "name_bio": "Imię i bio",
+    "new_email": "Nowy email",
     "new_password": "Nowe hasło",
     "notification_visibility": "Rodzaje powiadomień do wyświetlania",
     "notification_visibility_follows": "Obserwacje",
     "notification_visibility_likes": "Ulubione",
     "notification_visibility_mentions": "Wzmianki",
     "notification_visibility_repeats": "Powtórzenia",
+    "notification_visibility_moves": "Użytkownik migruje",
+    "notification_visibility_emoji_reactions": "Reakcje",
     "no_rich_text_description": "Usuwaj formatowanie ze wszystkich postów",
     "no_blocks": "Bez blokad",
     "no_mutes": "Bez wyciszeń",
     "hide_follows_description": "Nie pokazuj kogo obserwuję",
     "hide_followers_description": "Nie pokazuj kto mnie obserwuje",
+    "hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
+    "hide_followers_count_description": "Nie pokazuj licznika obserwujących",
     "show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
     "show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
     "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
@@ -212,10 +365,14 @@
     "reply_visibility_all": "Pokazuj wszystkie odpowiedzi",
     "reply_visibility_following": "Pokazuj tylko odpowiedzi skierowane do mnie i osób które obserwuję",
     "reply_visibility_self": "Pokazuj tylko odpowiedzi skierowane do mnie",
+    "autohide_floating_post_button": "Ukryj automatycznie przycisk \"Nowy post\" (mobile)",
     "saving_err": "Nie udało się zapisać ustawień",
     "saving_ok": "Zapisano ustawienia",
+    "search_user_to_block": "Wyszukaj kogo chcesz zablokować",
+    "search_user_to_mute": "Wyszukaj kogo chcesz wyciszyć",
     "security_tab": "Bezpieczeństwo",
     "scope_copy": "Kopiuj zakres podczas odpowiadania (DM-y zawsze są kopiowane)",
+    "minimal_scopes_mode": "Zminimalizuj opcje wyboru zakresu postów",
     "set_new_avatar": "Ustaw nowy awatar",
     "set_new_profile_background": "Ustaw nowe tło profilu",
     "set_new_profile_banner": "Ustaw nowy banner profilu",
@@ -228,19 +385,32 @@
     "post_status_content_type": "Post status content type",
     "stop_gifs": "Odtwarzaj GIFy po najechaniu kursorem",
     "streaming": "Włącz automatycznie strumieniowanie nowych postów gdy jesteś na początku strony",
+    "user_mutes": "Users",
+    "useStreamingApi": "Otrzymuj posty i powiadomienia w czasie rzeczywistym",
+    "useStreamingApiWarning": "(Niezalecane, eksperymentalne, pomija posty)",
     "text": "Tekst",
     "theme": "Motyw",
     "theme_help": "Użyj kolorów w notacji szesnastkowej (#rrggbb), by stworzyć swój motyw.",
     "theme_help_v2_1": "Możesz też zastąpić kolory i widoczność poszczególnych komponentów przełączając pola wyboru, użyj „Wyczyść wszystko” aby usunąć wszystkie zastąpienia.",
     "theme_help_v2_2": "Ikony pod niektórych wpisami są wskaźnikami kontrastu pomiędzy tłem a tekstem, po najechaniu na nie otrzymasz szczegółowe informacje. Zapamiętaj, że jeżeli używasz przezroczystości, wskaźniki pokazują najgorszy możliwy przypadek.",
     "tooltipRadius": "Etykiety/alerty",
+    "type_domains_to_mute": "Wpisz domeny, które chcesz wyciszyć",
     "upload_a_photo": "Wyślij zdjęcie",
     "user_settings": "Ustawienia użytkownika",
     "values": {
       "false": "nie",
       "true": "tak"
     },
+    "fun": "Zabawa",
+    "greentext": "Memiczne strzałki",
     "notifications": "Powiadomienia",
+    "notification_setting": "Otrzymuj powiadomienia od:",
+    "notification_setting_follows": "Ludzi których obserwujesz",
+    "notification_setting_non_follows": "Ludzi których nie obserwujesz",
+    "notification_setting_followers": "Ludzi którzy obserwują ciebie",
+    "notification_setting_non_followers": "Ludzi którzy nie obserwują ciebie",
+    "notification_mutes": "By przestać otrzymywać powiadomienia od jednego użytkownika, wycisz go",
+    "notification_blocks": "Blokowanie uzytkownika zatrzymuje wszystkie powiadomienia i odsubskrybowuje go.",
     "enable_web_push_notifications": "Włącz powiadomienia push",
     "style": {
       "switcher": {
@@ -252,7 +422,24 @@
         "save_load_hint": "Opcje „zachowaj” pozwalają na pozostanie przy obecnych opcjach po wybraniu lub załadowaniu motywu, jak i przechowywanie ich podczas eksportowania motywu. Jeżeli wszystkie są odznaczone, eksportowanie motywu spowoduje zapisanie wszystkiego.",
         "reset": "Wyzeruj",
         "clear_all": "Wyczyść wszystko",
-        "clear_opacity": "Wyczyść widoczność"
+        "clear_opacity": "Wyczyść widoczność",
+        "load_theme": "Załaduj motyw",
+        "keep_as_is": "Zostaw po staremu",
+        "use_snapshot": "Stara wersja",
+        "use_source": "Nowa wersja",
+        "help": {
+          "upgraded_from_v2": "PleromaFE zostało zaaktualizowane, motyw może wyglądać nieco inaczej niż sobie zapamiętałeś.",
+          "v2_imported": "Plik który zaimportowałeś został stworzony dla starszego FE. Próbujemy zwiększyć kompatybiliność, lecz wciąż mogą występować rozbieżności.",
+          "future_version_imported": "Plik który zaimportowałeś został stworzony w nowszej wersji FE.",
+          "older_version_imported": "Plik który zaimportowałeś został stworzony w starszej wersji FE.",
+          "snapshot_present": "Migawka motywu jest załadowana, więc wszystkie wartości zostały nadpisane. Zamiast tego, możesz załadować właściwe dane motywu",
+          "snapshot_missing": "Nie znaleziono migawki motywu w pliku, więc motyw może wyglądać inaczej niż pierwotnie zaplanowano.",
+          "fe_upgraded": "Silnik motywów PleromaFE został zaaktualizowany.",
+          "fe_downgraded": "Wersja PleromaFE została cofnięta.",
+          "migration_snapshot_ok": "Żeby być bezpiecznym, migawka motywu została załadowana. Możesz spróbować załadować dane motywu.",
+          "migration_napshot_gone": "Z jakiegoś powodu migawka zniknęła, niektóre rzeczy mogą wyglądać inaczej niż sobie zapamiętałeś.",
+          "snapshot_source_mismatch": "Konflikt wersji: najprawdopodobniej FE zostało cofnięte do poprzedniej wersji i zaaktualizowane ponownie, jeśli zmieniłeś motyw używając starszej wersji FE, najprawdopodobniej chcesz używać starszej wersji, w przeciwnym razie użyj nowej wersji."
+        }
       },
       "common": {
         "color": "Kolor",
@@ -280,14 +467,28 @@
         "_tab_label": "Zaawansowane",
         "alert": "Tło alertu",
         "alert_error": "Błąd",
+        "alert_warning": "Ostrzeżenie",
+        "alert_neutral": "Neutralne",
+        "post": "Posty/Bio użytkowników",
         "badge": "Tło odznaki",
+        "popover": "Etykiety, menu, popovery",
         "badge_notification": "Powiadomienie",
         "panel_header": "Nagłówek panelu",
         "top_bar": "Górny pasek",
         "borders": "Granice",
         "buttons": "Przyciski",
         "inputs": "Pola wejścia",
-        "faint_text": "Zanikający tekst"
+        "faint_text": "Zanikający tekst",
+        "underlay": "Podkład",
+        "poll": "Wykres ankiety",
+        "icons": "Ikony",
+        "highlight": "Podświetlone elementy",
+        "pressed": "Naciśnięte",
+        "selectedPost": "Wybrany post",
+        "selectedMenu": "Wybrany element menu",
+        "disabled": "Wyłączone",
+        "toggled": "Przełączone",
+        "tabs": "Karty"
       },
       "radii": {
         "_tab_label": "Zaokrąglenie"
@@ -300,7 +501,7 @@
         "blur": "Rozmycie",
         "spread": "Szerokość",
         "inset": "Inset",
-        "hint": "Możesz też używać --zmiennych jako kolorów, aby wykorzystać zmienne CSS3. Pamiętaj, że ustawienie widoczności nie będzie wtedy działać.",
+        "hintV3": "Dla cieni możesz również użyć notacji {0} by użyć inny slot koloru.",
         "filter_hint": {
           "always_drop_shadow": "Ostrzeżenie, ten cień zawsze używa {0} jeżeli to obsługiwane przez przeglądarkę.",
           "drop_shadow_syntax": "{0} nie obsługuje parametru {1} i słowa kluczowego {2}.",
@@ -357,6 +558,40 @@
       "frontend_version": "Wersja front-endu"
     }
   },
+  "time": {
+    "day": "{0} dzień",
+    "days": "{0} dni",
+    "day_short": "{0}d",
+    "days_short": "{0}d",
+    "hour": "{0} godzina",
+    "hours": "{0} godzin",
+    "hour_short": "{0} godz.",
+    "hours_short": "{0} godz.",
+    "in_future": "za {0}",
+    "in_past": "{0} temu",
+    "minute": "{0} minuta",
+    "minutes": "{0} minut",
+    "minute_short": "{0}min",
+    "minutes_short": "{0}min",
+    "month": "{0} miesiąc",
+    "months": "{0} miesięcy",
+    "month_short": "{0} mies.",
+    "months_short": "{0} mies.",
+    "now": "teraz",
+    "now_short": "teraz",
+    "second": "{0} sekunda",
+    "seconds": "{0} sekund",
+    "second_short": "{0}s",
+    "seconds_short": "{0}s",
+    "week": "{0} tydzień",
+    "weeks": "{0} tygodni",
+    "week_short": "{0} tydz.",
+    "weeks_short": "{0} tyg.",
+    "year": "{0} rok",
+    "years": "{0} lata",
+    "year_short": "{0} r.",
+    "years_short": "{0} lata"
+  },
   "timeline": {
     "collapse": "Zwiń",
     "conversation": "Rozmowa",
@@ -370,8 +605,17 @@
     "no_statuses": "Brak statusów"
   },
   "status": {
+    "favorites": "Ulubione",
+    "repeats": "Powtórzenia",
+    "delete": "Usuń status",
+    "pin": "Przypnij na profilu",
+    "unpin": "Odepnij z profilu",
+    "pinned": "Przypnięte",
+    "delete_confirm": "Czy naprawdę chcesz usunąć ten status?",
     "reply_to": "Odpowiedź dla",
-    "replies_list": "Odpowiedzi:"
+    "replies_list": "Odpowiedzi:",
+    "mute_conversation": "Wycisz konwersację",
+    "unmute_conversation": "Odcisz konwersację"
   },
   "user_card": {
     "approve": "Przyjmij",
@@ -388,25 +632,60 @@
     "followers": "Obserwujący",
     "following": "Obserwowany!",
     "follows_you": "Obserwuje cię!",
+    "hidden": "Ukryte",
     "its_you": "To ty!",
     "media": "Media",
+    "mention": "Wspomnienie",
     "mute": "Wycisz",
     "muted": "Wyciszony(-a)",
     "per_day": "dziennie",
     "remote_follow": "Zdalna obserwacja",
+    "report": "Raportuj",
     "statuses": "Statusy",
+    "subscribe": "Subskrybuj",
+    "unsubscribe": "Odsubskrybuj",
     "unblock": "Odblokuj",
     "unblock_progress": "Odblokowuję…",
     "block_progress": "Blokuję…",
     "unmute": "Cofnij wyciszenie",
     "unmute_progress": "Cofam wyciszenie…",
-    "mute_progress": "Wyciszam…"
+    "mute_progress": "Wyciszam…",
+    "hide_repeats": "Ukryj powtórzenia",
+    "show_repeats": "Pokaż powtórzenia",
+    "admin_menu": {
+      "moderation": "Moderacja",
+      "grant_admin": "Przyznaj admina",
+      "revoke_admin": "Odwołaj admina",
+      "grant_moderator": "Przyznaj moderatora",
+      "revoke_moderator": "Odwołaj moderatora",
+      "activate_account": "Aktywuj konto",
+      "deactivate_account": "Dezaktywuj konto",
+      "delete_account": "Usuń konto",
+      "force_nsfw": "Oznacz wszystkie posty jako NSFW",
+      "strip_media": "Usuń multimedia z postów",
+      "force_unlisted": "Wymuś posty na niepubliczne",
+      "sandbox": "Wymuś by posty były tylko dla obserwujących",
+      "disable_remote_subscription": "Zakaż obserwowania użytkownika ze zdalnych instancji",
+      "disable_any_subscription": "Zakaż całkowicie obserwowania użytkownika",
+      "quarantine": "Zakaż federowania postów od tego użytkownika",
+      "delete_user": "Usuń użytkownika",
+      "delete_user_confirmation": "Czy jesteś absolutnie pewny? Ta operacja nie może być cofnięta."
+    }
   },
   "user_profile": {
     "timeline_title": "Oś czasu użytkownika",
     "profile_does_not_exist": "Przepraszamy, ten profil nie istnieje.",
     "profile_loading_error": "Przepraszamy, wystąpił błąd podczas ładowania tego profilu."
   },
+  "user_reporting": {
+    "title": "Raportowanie {0}",
+    "add_comment_description": "Raport zostanie wysłany do moderatorów instancji. Możesz dodać powód dlaczego raportujesz to konto poniżej:",
+    "additional_comments": "Dodatkowe komentarze",
+    "forward_description": "To konto jest z innego serwera. Wysłać również tam kopię raportu?",
+    "forward_to": "Przekaż do{0}",
+    "submit": "Wyślij",
+    "generic_error": "Wystąpił błąd podczas przetwarzania twojej prośby."
+  },
   "who_to_follow": {
     "more": "Więcej",
     "who_to_follow": "Propozycje obserwacji"
@@ -416,6 +695,7 @@
     "repeat": "Powtórz",
     "reply": "Odpowiedz",
     "favorite": "Dodaj do ulubionych",
+    "add_reaction": "Dodaj reakcję",
     "user_settings": "Ustawienia użytkownika"
   },
   "upload":{
@@ -431,5 +711,25 @@
       "GiB": "GiB",
       "TiB": "TiB"
     }
+  },
+  "search": {
+    "people": "Ludzie",
+    "hashtags": "Hasztagi",
+    "person_talking": "{count} osoba rozmawia o tym",
+    "people_talking": "{count} osób rozmawia o tym",
+    "no_results": "Brak wyników"
+  },
+  "password_reset": {
+    "forgot_password": "Zapomniałeś hasła?",
+    "password_reset": "Reset hasła",
+    "instruction": "Wprowadź swój adres email lub nazwę użytkownika. Wyślemy ci link z którym możesz zresetować hasło.",
+    "placeholder": "Twój email lub nazwa użytkownika",
+    "check_email": "Sprawdź pocztę, aby uzyskać link do zresetowania hasła.",
+    "return_home": "Wróć do strony głównej",
+    "not_found": "Nie mogliśmy znaleźć tego emaila lub nazwy użytkownika.",
+    "too_many_requests": "Przekroczyłeś limit prób, spróbuj ponownie później.",
+    "password_reset_disabled": "Resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji.",
+    "password_reset_required": "Musisz zresetować hasło, by się zalogować.",
+    "password_reset_required_but_mailer_is_disabled": "Musisz zresetować hasło, ale resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji."
   }
 }
diff --git a/src/modules/config.js b/src/modules/config.js
index 7997521d..8f4638f5 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -34,7 +34,8 @@ export const defaultState = {
     likes: true,
     repeats: true,
     moves: true,
-    emojiReactions: false
+    emojiReactions: false,
+    followRequest: true
   },
   webPushNotifications: false,
   muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f1b7dcbd..cd8c1dba 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,6 +13,7 @@ import {
   omitBy
 } from 'lodash'
 import { set } from 'vue'
+import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
 
@@ -321,7 +322,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
 
 const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
   each(notifications, (notification) => {
-    if (notification.type !== 'follow' && notification.type !== 'move') {
+    if (isStatusNotification(notification.type)) {
       notification.action = addStatusToGlobalStorage(state, notification.action).item
       notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
     }
@@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
           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 {
+        } else if (isStatusNotification(notification.type)) {
           notifObj.body = notification.status.text
         }
 
@@ -521,6 +525,17 @@ export const mutations = {
       notification.seen = true
     })
   },
+  markSingleNotificationAsSeen (state, { id }) {
+    const notification = find(state.notifications.data, n => n.id === id)
+    if (notification) notification.seen = true
+  },
+  dismissNotification (state, { id }) {
+    state.notifications.data = state.notifications.data.filter(n => n.id !== id)
+  },
+  updateNotification (state, { id, updater }) {
+    const notification = find(state.notifications.data, n => n.id === id)
+    notification && updater(notification)
+  },
   queueFlush (state, { timeline, id }) {
     state.timelines[timeline].flushMarker = id
   },
@@ -680,6 +695,24 @@ const statuses = {
         credentials: rootState.users.currentUser.credentials
       })
     },
+    markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
+      commit('markSingleNotificationAsSeen', { id })
+      apiService.markNotificationsAsSeen({
+        single: true,
+        id,
+        credentials: rootState.users.currentUser.credentials
+      })
+    },
+    dismissNotificationLocal ({ rootState, commit }, { id }) {
+      commit('dismissNotification', { id })
+    },
+    dismissNotification ({ rootState, commit }, { id }) {
+      commit('dismissNotification', { id })
+      rootState.api.backendInteractor.dismissNotification({ id })
+    },
+    updateNotification ({ rootState, commit }, { id, updater }) {
+      commit('updateNotification', { id, updater })
+    },
     fetchFavsAndRepeats ({ rootState, commit }, id) {
       Promise.all([
         rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7db1d094..72c8874f 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -4,7 +4,6 @@ import 'whatwg-fetch'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
-const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
 const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
 const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
 const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@@ -17,6 +16,7 @@ const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
 const ADMIN_USERS_URL = '/api/pleroma/admin/users'
 const SUGGESTIONS_URL = '/api/v1/suggestions'
 const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
+const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
 
 const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
 const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
 const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
 const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
 const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
+const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
 const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
 const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
 const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@@ -495,8 +496,7 @@ const fetchTimeline = ({
   until = false,
   userId = false,
   tag = false,
-  withMuted = false,
-  withMove = false
+  withMuted = false
 }) => {
   const timelineUrls = {
     public: MASTODON_PUBLIC_TIMELINE,
@@ -536,11 +536,8 @@ const fetchTimeline = ({
   if (timeline === 'public' || timeline === 'publicAndExternal') {
     params.push(['only_media', false])
   }
-  if (timeline === 'notifications') {
-    params.push(['with_move', withMove])
-  }
 
-  params.push(['count', 20])
+  params.push(['limit', 20])
   params.push(['with_muted', withMuted])
 
   const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
@@ -844,12 +841,16 @@ const suggestions = ({ credentials }) => {
   }).then((data) => data.json())
 }
 
-const markNotificationsAsSeen = ({ id, credentials }) => {
+const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
   const body = new FormData()
 
-  body.append('latest_id', id)
+  if (single) {
+    body.append('id', id)
+  } else {
+    body.append('max_id', id)
+  }
 
-  return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
+  return fetch(NOTIFICATION_READ_URL, {
     body,
     headers: authHeaders(credentials),
     method: 'POST'
@@ -1010,6 +1011,15 @@ const unmuteDomain = ({ domain, credentials }) => {
   })
 }
 
+const dismissNotification = ({ credentials, id }) => {
+  return promisedRequest({
+    url: MASTODON_DISMISS_NOTIFICATION_URL(id),
+    method: 'POST',
+    payload: { id },
+    credentials
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1165,6 +1175,7 @@ const apiService = {
   denyUser,
   suggestions,
   markNotificationsAsSeen,
+  dismissNotification,
   vote,
   fetchPoll,
   fetchFavoritedByUsers,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 724dcc4b..6dac7c15 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,4 +1,5 @@
 import escape from 'escape-html'
+import { isStatusNotification } from '../notification_utils/notification_utils.js'
 
 const qvitterStatusType = (status) => {
   if (status.is_post_verb) {
@@ -340,9 +341,7 @@ export const parseNotification = (data) => {
   if (masto) {
     output.type = mastoDict[data.type] || data.type
     output.seen = data.pleroma.is_seen
-    output.status = output.type === 'follow' || output.type === 'move'
-      ? null
-      : parseStatus(data.status)
+    output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
     output.action = output.status // TODO: Refactor, this is unneeded
     output.target = output.type !== 'move'
       ? null
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index b17bd7bf..eb479227 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,4 +1,4 @@
-import { filter, sortBy } from 'lodash'
+import { filter, sortBy, includes } from 'lodash'
 
 export const notificationsFromStore = store => store.state.statuses.notifications.data
 
@@ -7,10 +7,15 @@ export const visibleTypes = store => ([
   store.state.config.notificationVisibility.mentions && 'mention',
   store.state.config.notificationVisibility.repeats && 'repeat',
   store.state.config.notificationVisibility.follows && 'follow',
+  store.state.config.notificationVisibility.followRequest && 'follow_request',
   store.state.config.notificationVisibility.moves && 'move',
   store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
 ].filter(_ => _))
 
+const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
+
+export const isStatusNotification = (type) => includes(statusNotifications, type)
+
 const sortById = (a, b) => {
   const seqA = Number(a.id)
   const seqB = Number(b.id)
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 864e32f8..64499a1b 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -11,12 +11,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
   const rootState = store.rootState || store.state
   const timelineData = rootState.statuses.notifications
   const hideMutedPosts = getters.mergedConfig.hideMutedPosts
-  const allowFollowingMove = rootState.users.currentUser.allow_following_move
 
   args['withMuted'] = !hideMutedPosts
 
-  args['withMove'] = !allowFollowingMove
-
   args['timeline'] = 'notifications'
   if (older) {
     if (timelineData.minId !== Number.POSITIVE_INFINITY) {
diff --git a/static/fontello.json b/static/fontello.json
index 5a7086a2..7f0e7cdd 100755
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -345,6 +345,24 @@
       "css": "link",
       "code": 59427,
       "src": "fontawesome"
+    },
+    {
+      "uid": "4aad6bb50b02c18508aae9cbe14e784e",
+      "css": "share",
+      "code": 61920,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "8b80d36d4ef43889db10bc1f0dc9a862",
+      "css": "user",
+      "code": 59428,
+      "src": "fontawesome"
+    },
+    {
+      "uid": "12f4ece88e46abd864e40b35e05b11cd",
+      "css": "ok",
+      "code": 59431,
+      "src": "fontawesome"
     }
   ]
-}
+}
\ No newline at end of file