diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e3eaf1730a04b5d103f048c9627f5134a65130a..887588f32f0e8e3a659941381de54dea958844b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### 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)
@@ -16,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - 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
+- Added profile meta data fields that can be set in profile settings
 
 ### Changed
 - Registration page no longer requires email if the server is configured not to require it
@@ -25,12 +28,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - 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/src/boot/after_store.js b/src/boot/after_store.js
index 0db035475f7f8ce7ba9f59492da471cd2b7e3413..1796eb1be9b42b6093768608d1c99e861add8721 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -8,38 +8,64 @@ import backendInteractorService from '../services/backend_interactor_service/bac
 import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
 import { applyTheme } from '../services/style_setter/style_setter.js'
 
-const getStatusnetConfig = async ({ store }) => {
+let staticInitialResults = null
+
+const parsedInitialResults = () => {
+  if (!document.getElementById('initial-results')) {
+    return null
+  }
+  if (!staticInitialResults) {
+    staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
+  }
+  return staticInitialResults
+}
+
+const preloadFetch = async (request) => {
+  const data = parsedInitialResults()
+  if (!data || !data[request]) {
+    return window.fetch(request)
+  }
+  const requestData = atob(data[request])
+  return {
+    ok: true,
+    json: () => JSON.parse(requestData),
+    text: () => requestData
+  }
+}
+
+const getInstanceConfig = async ({ store }) => {
   try {
-    const res = await window.fetch('/api/statusnet/config.json')
+    const res = await preloadFetch('/api/v1/instance')
     if (res.ok) {
       const data = await res.json()
-      const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
-
-      store.dispatch('setInstanceOption', { name: 'name', value: name })
-      store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
-      store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
-      store.dispatch('setInstanceOption', { name: 'server', value: server })
-      store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
-
-      // TODO: default values for this stuff, added if to not make it break on
-      // my dev config out of the box.
-      if (uploadlimit) {
-        store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
-        store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
-        store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
-        store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
-      }
+      const textlimit = data.max_toot_chars
+      const vapidPublicKey = data.pleroma.vapid_public_key
+
+      store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
 
       if (vapidPublicKey) {
         store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
       }
+    } else {
+      throw (res)
+    }
+  } catch (error) {
+    console.error('Could not load instance config, potentially fatal')
+    console.error(error)
+  }
+}
 
-      return data.site.pleromafe
+const getBackendProvidedConfig = async ({ store }) => {
+  try {
+    const res = await window.fetch('/api/pleroma/frontend_configurations')
+    if (res.ok) {
+      const data = await res.json()
+      return data.pleroma_fe
     } else {
       throw (res)
     }
   } catch (error) {
-    console.error('Could not load statusnet config, potentially fatal')
+    console.error('Could not load backend-provided frontend config, potentially fatal')
     console.error(error)
   }
 }
@@ -132,7 +158,7 @@ const getTOS = async ({ store }) => {
 
 const getInstancePanel = async ({ store }) => {
   try {
-    const res = await window.fetch('/instance/panel.html')
+    const res = await preloadFetch('/instance/panel.html')
     if (res.ok) {
       const html = await res.text()
       store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
@@ -195,18 +221,28 @@ const resolveStaffAccounts = ({ store, accounts }) => {
 
 const getNodeInfo = async ({ store }) => {
   try {
-    const res = await window.fetch('/nodeinfo/2.0.json')
+    const res = await preloadFetch('/nodeinfo/2.0.json')
     if (res.ok) {
       const data = await res.json()
       const metadata = data.metadata
       const features = metadata.features
+      store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
+      store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
       store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
+      store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
       store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
       store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
       store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
       store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
       store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
 
+      const uploadLimits = metadata.uploadLimits
+      store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
+      store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
+      store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
+      store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
+      store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
+
       store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
       store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
 
@@ -257,7 +293,7 @@ const getNodeInfo = async ({ store }) => {
 
 const setConfig = async ({ store }) => {
   // apiConfig, staticConfig
-  const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()])
+  const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
   const apiConfig = configInfos[0]
   const staticConfig = configInfos[1]
 
@@ -280,6 +316,11 @@ const checkOAuthToken = async ({ store }) => {
 const afterStoreSetup = async ({ store, i18n }) => {
   const width = windowWidth()
   store.dispatch('setMobileLayout', width <= 800)
+
+  const overrides = window.___pleromafe_dev_overrides || {}
+  const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
+  store.dispatch('setInstanceOption', { name: 'server', value: server })
+
   await setConfig({ store })
 
   const { customTheme, customThemeSource } = store.state.config
@@ -299,16 +340,18 @@ const afterStoreSetup = async ({ store, i18n }) => {
   }
 
   // Now we can try getting the server settings and logging in
+  // Most of these are preloaded into the index.html so blocking is minimized
   await Promise.all([
     checkOAuthToken({ store }),
-    getTOS({ store }),
     getInstancePanel({ store }),
-    getStickers({ store }),
-    getNodeInfo({ store })
+    getNodeInfo({ store }),
+    getInstanceConfig({ store })
   ])
 
   // Start fetching things that don't need to block the UI
   store.dispatch('fetchMutes')
+  getTOS({ store })
+  getStickers({ store })
 
   const router = new VueRouter({
     mode: 'history',
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 744b77d5399b9dba509389ecc73c7ac7501e41e7..029e70968a0d69b90b0bfdd311c148f44e99ca65 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/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index f4c3479c7d6c3abaf3c079bd9b3484df22e3688b..7974a66d93c6853e8e6115367a311c152b1ba42a 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -431,6 +431,7 @@ const EmojiInput = {
       const offsetBottom = offsetTop + offsetHeight
 
       panel.style.top = offsetBottom + 'px'
+      if (!picker) return
       picker.$el.style.top = offsetBottom + 'px'
       picker.$el.style.bottom = 'auto'
     }
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 15a71effa1f6055bd658ba5443a543419b68e680..8330345bb093d7515c57502d754939f17ecbd93f 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 bca93ea7448fe381fadeee4309ff14d034a770a2..68db6fd8f1159a5452092dff310a3ea0e61889b9 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/poll/poll.vue b/src/components/poll/poll.vue
index 56e91cca502f800cf870a3661a034937af2f897f..adbb05558e3d28a6c0ac95fb7cdc47c0c4e02848 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 5881d266b45bd9510059d8b4fa9d7e798d538c22..a40a919521aeb75e8d5dfaf132fb05a7e6d80d04 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/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 8658b09770a7a2765c6cd8a095358adf644a7b87..e6db802ddb5c7de4f995f86f78d432074fd14826 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -1,4 +1,5 @@
 import unescape from 'lodash/unescape'
+import merge from 'lodash/merge'
 import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
 import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
 import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
@@ -16,6 +17,7 @@ const ProfileTab = {
       newLocked: this.$store.state.users.currentUser.locked,
       newNoRichText: this.$store.state.users.currentUser.no_rich_text,
       newDefaultScope: this.$store.state.users.currentUser.default_scope,
+      newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
       hideFollows: this.$store.state.users.currentUser.hide_follows,
       hideFollowers: this.$store.state.users.currentUser.hide_followers,
       hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
@@ -23,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,
@@ -62,6 +65,18 @@ const ProfileTab = {
         ...this.$store.state.instance.emoji,
         ...this.$store.state.instance.customEmoji
       ] })
+    },
+    userSuggestor () {
+      return suggestor({
+        users: this.$store.state.users.users,
+        updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
+      })
+    },
+    fieldsLimits () {
+      return this.$store.state.instance.fieldsLimits
+    },
+    maxFields () {
+      return this.fieldsLimits ? this.fieldsLimits.maxFields : 0
     }
   },
   methods: {
@@ -74,17 +89,21 @@ const ProfileTab = {
             // Backend notation.
             /* eslint-disable camelcase */
             display_name: this.newName,
+            fields_attributes: this.newFields.filter(el => el != null),
             default_scope: this.newDefaultScope,
             no_rich_text: this.newNoRichText,
             hide_follows: this.hideFollows,
             hide_followers: this.hideFollowers,
             discoverable: this.discoverable,
+            bot: this.bot,
             allow_following_move: this.allowFollowingMove,
             hide_follows_count: this.hideFollowsCount,
             hide_followers_count: this.hideFollowersCount,
             show_role: this.showRole
             /* eslint-enable camelcase */
           } }).then((user) => {
+          this.newFields.splice(user.fields.length)
+          merge(this.newFields, user.fields)
           this.$store.commit('addNewUsers', [user])
           this.$store.commit('setCurrentUser', user)
         })
@@ -92,6 +111,16 @@ const ProfileTab = {
     changeVis (visibility) {
       this.newDefaultScope = visibility
     },
+    addField () {
+      if (this.newFields.length < this.maxFields) {
+        this.newFields.push({ name: '', value: '' })
+        return true
+      }
+      return false
+    },
+    deleteField (index, event) {
+      this.$delete(this.newFields, index)
+    },
     uploadFile (slot, e) {
       const file = e.target.files[0]
       if (!file) { return }
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index 4aab81eb7e04ddf868bae5c09732c1d3031fb1f8..b3dcf42c865238cc54a079c5910dbbb7d9e9f10b 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -79,4 +79,21 @@
   .setting-subitem {
     margin-left: 1.75em;
   }
+
+  .profile-fields {
+    display: flex;
+
+    &>.emoji-input {
+      flex: 1 1 auto;
+      margin: 0 .2em .5em;
+    }
+
+    &>.icon-container {
+      width: 20px;
+
+      &>.icon-cancel {
+        vertical-align: sub;
+      }
+    }
+  }
 }
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index fff4f970c0caa367844fec09211f1207102b6b69..0f9210a6d1947c76c337ce8d2d5f2800a23a3688 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -95,6 +95,59 @@
           {{ $t('settings.discoverable') }}
         </Checkbox>
       </p>
+      <div v-if="maxFields > 0">
+        <p>{{ $t('settings.profile_fields.label') }}</p>
+        <div
+          v-for="(_, i) in newFields"
+          :key="i"
+          class="profile-fields"
+        >
+          <EmojiInput
+            v-model="newFields[i].name"
+            enable-emoji-picker
+            hide-emoji-button
+            :suggest="userSuggestor"
+          >
+            <input
+              v-model="newFields[i].name"
+              :placeholder="$t('settings.profile_fields.name')"
+            >
+          </EmojiInput>
+          <EmojiInput
+            v-model="newFields[i].value"
+            enable-emoji-picker
+            hide-emoji-button
+            :suggest="userSuggestor"
+          >
+            <input
+              v-model="newFields[i].value"
+              :placeholder="$t('settings.profile_fields.value')"
+            >
+          </EmojiInput>
+          <div
+            class="icon-container"
+          >
+            <i
+              v-show="newFields.length > 1"
+              class="icon-cancel"
+              @click="deleteField(i)"
+            />
+          </div>
+        </div>
+        <a
+          v-if="newFields.length < maxFields"
+          class="add-field faint"
+          @click="addField"
+        >
+          <i class="icon-plus" />
+          {{ $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/status/status.vue b/src/components/status/status.vue
index 336f912a7064ad3a60b161f48d46cc97b9073c47..7ec29b2884c2d5d34671c9cfaee9ec8d015a13d6 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 7adb67ae34c42555816384405737b6e47682c6ae..efc2485ead4037181637e2ee4236ae04f42ec585 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;
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index c4a5ce9d6d3af565c8372bc16eb91148a077abb7..9529d7f6c0affea540981cf932598a458a41e539 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 95760bf84b5ae3847de6451618e6c4fff80b0008..201727d49a046cdd90a8e5fca43a9aa7de2a1c4f 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 1871d46c6200f790eab2ae7be8e223a4ef90e7fb..361a3b5c80b2db5ded883e30f76766a18bd9f21a 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 eefe10e5ea34cc2e0cde5f2b6584478ef97bf52d..2840904fc0c1ad7aa879e81fa7f9cb475ee1c175 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)",
@@ -333,6 +334,12 @@
     "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
     "mutes_tab": "Mutes",
     "play_videos_in_modal": "Play videos in a popup frame",
+    "profile_fields": {
+      "label": "Profile metadata",
+      "add_field": "Add Field",
+      "name": "Label",
+      "value": "Content"
+    },
     "use_contain_fit": "Don't crop the attachment in thumbnails",
     "name": "Name",
     "name_bio": "Name & Bio",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 6c8be351f80887f783e1dcbe4d128731d174347d..7311f0b69ed9308644f9df0720814cc851f7388a 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -255,7 +255,8 @@
         "top_bar": "Barra superiore",
         "panel_header": "Titolo pannello",
         "badge_notification": "Notifica",
-        "popover": "Suggerimenti, menù, sbalzi"
+        "popover": "Suggerimenti, menù, sbalzi",
+        "toggled": "Scambiato"
       },
       "common_colors": {
         "rgbo": "Icone, accenti, medaglie",
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index af728b6eb7fd7d196b932c9d180f1fa0321b11b2..bf270f87d8dfdc2a300f420134bcd3e6c55e4969 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -28,7 +28,12 @@
     "enable": "Inschakelen",
     "confirm": "Bevestigen",
     "verify": "Verifiëren",
-    "generic_error": "Er is een fout opgetreden"
+    "generic_error": "Er is een fout opgetreden",
+    "peek": "Spiek",
+    "close": "Sluiten",
+    "retry": "Opnieuw proberen",
+    "error_retry": "Probeer het opnieuw",
+    "loading": "Laden…"
   },
   "login": {
     "login": "Log in",
@@ -90,7 +95,7 @@
       "text/bbcode": "BBCode"
     },
     "content_warning": "Onderwerp (optioneel)",
-    "default": "Zojuist geland in L.A.",
+    "default": "Tijd voor anime!",
     "direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.",
     "posting": "Plaatsen",
     "scope": {
@@ -377,7 +382,7 @@
         "button": "Knop",
         "text": "Nog een boel andere {0} en {1}",
         "mono": "inhoud",
-        "input": "Zojuist geland in L.A.",
+        "input": "Tijd voor anime!",
         "faint_link": "handige gebruikershandleiding",
         "fine_print": "Lees onze {0} om niets nuttig te leren!",
         "header_faint": "Alles komt goed",
@@ -451,7 +456,7 @@
     "user_mutes": "Gebruikers",
     "useStreamingApi": "Berichten en meldingen in real-time ontvangen",
     "useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)",
-    "type_domains_to_mute": "Voer domeinen in om te negeren",
+    "type_domains_to_mute": "Zoek domeinen om te negeren",
     "upload_a_photo": "Upload een foto",
     "fun": "Plezier",
     "greentext": "Meme pijlen",
@@ -470,7 +475,15 @@
       "frontend_version": "Frontend Versie",
       "backend_version": "Backend Versie",
       "title": "Versie"
-    }
+    },
+    "mutes_and_blocks": "Negeringen en Blokkades",
+    "profile_fields": {
+      "value": "Inhoud",
+      "name": "Label",
+      "add_field": "Veld Toevoegen",
+      "label": "Profiel metadata"
+    },
+    "bot": "Dit is een bot account"
   },
   "timeline": {
     "collapse": "Inklappen",
@@ -708,7 +721,9 @@
     "unpin": "Van profiel losmaken",
     "delete": "Status verwijderen",
     "repeats": "Herhalingen",
-    "favorites": "Favorieten"
+    "favorites": "Favorieten",
+    "thread_muted_and_words": ", heeft woorden:",
+    "thread_muted": "Thread genegeerd"
   },
   "time": {
     "years_short": "{0}j",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index f9a729544fde979edb1daf35fbaf701df74bd6ce..aa78db26e3ead4b439ac201432047f86c178b1cb 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -130,6 +130,7 @@
     "background": "Фон",
     "bio": "Описание",
     "btnRadius": "Кнопки",
+    "bot": "Это аккаунт бота",
     "cBlue": "Ответить, читать",
     "cGreen": "Повторить",
     "cOrange": "Нравится",
diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js
new file mode 100644
index 0000000000000000000000000000000000000000..270ed043c59d142a9e2dbfb06cc7cd057d294a61
--- /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 0000000000000000000000000000000000000000..71f9156a87594b589035451c0f91656d602bccd5
--- /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/modules/statuses.js b/src/modules/statuses.js
index 9a2e0df1e9646427995bd2573dfa024465fdcde3..073b15f1b6174132240270e2145c6a975e67ddc4 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 f9329f2a81b5b6419051b6b389ceb8fcee3536f7..68d029315c134981441060ad28684bf8cac538cb 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,6 +1,6 @@
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
 import oauthApi from '../services/new_api/oauth.js'
-import { compact, map, each, merge, last, concat, uniq } from 'lodash'
+import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
 import { set } from 'vue'
 import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
 
@@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => {
   const oldItem = obj[item.id]
   if (oldItem) {
     // We already have this, so only merge the new info.
-    merge(oldItem, item)
+    mergeWith(oldItem, item, mergeArrayLength)
     return { item: oldItem, new: false }
   } else {
     // This is a new item, prepare it
@@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => {
   }
 }
 
+const mergeArrayLength = (oldValue, newValue) => {
+  if (isArray(oldValue) && isArray(newValue)) {
+    oldValue.length = newValue.length
+    return mergeWith(oldValue, newValue, mergeArrayLength)
+  }
+}
+
 const getNotificationPermission = () => {
   const Notification = window.Notification
 
@@ -116,7 +123,7 @@ export const mutations = {
   },
   setCurrentUser (state, user) {
     state.lastLoginName = user.screen_name
-    state.currentUser = merge(state.currentUser || {}, user)
+    state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
   },
   clearCurrentUser (state) {
     state.currentUser = false
@@ -428,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/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index c7ed65a4d42300bba9be1e4f3781a137f4c75b53..3bdb92f3ff2e923986eb700ab3b09e6f61d6e492 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 eb479227ca4749737f1b525a4aaa6965dd5f9db6..5cc19215aaf388c1c7852d47455b6cb9525fb1e3 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/sw.js b/src/sw.js
index 6cecb3f380004f918b313f2068ac67541fdeccb1..f5e34dd608787c0c3db66226049ac05fdcbe6858 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
+}
+
+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)
 
-        if (list.length === 0) return self.registration.showNotification(data.title, data)
-      })
-    }))
+    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/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js
index 670acfc8221251431ce3ed5879fd3b0f2e2b91db..dfa5684d15bc5def3973d318a7339b6ac6952ef4 100644
--- a/test/unit/specs/modules/users.spec.js
+++ b/test/unit/specs/modules/users.spec.js
@@ -18,6 +18,42 @@ describe('The users module', () => {
       expect(state.users).to.eql([user])
       expect(state.users[0].name).to.eql('Dude')
     })
+
+    it('merging array field in new information for old users', () => {
+      const state = cloneDeep(defaultState)
+      const user = {
+        id: '1',
+        fields: [
+          { name: 'Label 1', value: 'Content 1' }
+        ]
+      }
+      const firstModUser = {
+        id: '1',
+        fields: [
+          { name: 'Label 2', value: 'Content 2' },
+          { name: 'Label 3', value: 'Content 3' }
+        ]
+      }
+      const secondModUser = {
+        id: '1',
+        fields: [
+          { name: 'Label 4', value: 'Content 4' }
+        ]
+      }
+
+      mutations.addNewUsers(state, [user])
+      expect(state.users[0].fields).to.have.length(1)
+      expect(state.users[0].fields[0].name).to.eql('Label 1')
+
+      mutations.addNewUsers(state, [firstModUser])
+      expect(state.users[0].fields).to.have.length(2)
+      expect(state.users[0].fields[0].name).to.eql('Label 2')
+      expect(state.users[0].fields[1].name).to.eql('Label 3')
+
+      mutations.addNewUsers(state, [secondModUser])
+      expect(state.users[0].fields).to.have.length(1)
+      expect(state.users[0].fields[0].name).to.eql('Label 4')
+    })
   })
 
   describe('findUser', () => {
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 166fce2b7a37f934521ac4720dbaaffebd6e4692..ccb57942c871da6d8be6657e0e386dde88743096 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 } })