about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/test-js.yml4
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock18
-rw-r--r--app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb72
-rw-r--r--app/controllers/api/v1/admin/trends/links_controller.rb31
-rw-r--r--app/controllers/api/v1/admin/trends/statuses_controller.rb31
-rw-r--r--app/controllers/api/v1/admin/trends/tags_controller.rb23
-rw-r--r--app/controllers/auth/setup_controller.rb19
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/javascript/flavours/glitch/components/animated_number.jsx76
-rw-r--r--app/javascript/flavours/glitch/components/animated_number.tsx58
-rw-r--r--app/javascript/flavours/glitch/components/gifv.jsx76
-rw-r--r--app/javascript/flavours/glitch/components/gifv.tsx68
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx2
-rw-r--r--app/javascript/flavours/glitch/features/status/index.jsx10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.jsx2
-rw-r--r--app/javascript/flavours/glitch/packs/public.jsx50
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss86
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap6
-rw-r--r--app/javascript/mastodon/components/animated_number.jsx76
-rw-r--r--app/javascript/mastodon/components/animated_number.tsx58
-rw-r--r--app/javascript/mastodon/components/avatar_overlay.jsx51
-rw-r--r--app/javascript/mastodon/components/avatar_overlay.tsx51
-rw-r--r--app/javascript/mastodon/components/gifv.jsx76
-rw-r--r--app/javascript/mastodon/components/gifv.tsx68
-rw-r--r--app/javascript/mastodon/components/status.jsx2
-rw-r--r--app/javascript/mastodon/components/status_content.jsx6
-rw-r--r--app/javascript/mastodon/features/direct_timeline/components/conversation.jsx2
-rw-r--r--app/javascript/mastodon/features/status/index.jsx10
-rw-r--r--app/javascript/mastodon/features/ui/components/filter_modal.jsx2
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.jsx2
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.jsx2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json21
-rw-r--r--app/javascript/mastodon/locales/en.json4
-rw-r--r--app/javascript/mastodon/locales/es-MX.json1
-rw-r--r--app/javascript/packs/public.jsx50
-rw-r--r--app/javascript/styles/mastodon/forms.scss86
-rw-r--r--app/mailers/notification_mailer.rb10
-rw-r--r--app/models/account_filter.rb2
-rw-r--r--app/models/preview_card_provider.rb1
-rw-r--r--app/serializers/rest/admin/trends/link_serializer.rb9
-rw-r--r--app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb10
-rw-r--r--app/serializers/rest/admin/trends/status_serializer.rb9
-rw-r--r--app/services/notify_service.rb1
-rw-r--r--app/views/auth/registrations/new.html.haml8
-rw-r--r--app/views/auth/registrations/rules.html.haml2
-rw-r--r--app/views/auth/setup/show.html.haml26
-rw-r--r--app/views/auth/shared/_links.html.haml2
-rw-r--r--app/views/auth/shared/_progress.html.haml25
-rw-r--r--config/locales/en.yml22
-rw-r--r--config/locales/simple_form.an.yml1
-rw-r--r--config/locales/simple_form.ar.yml1
-rw-r--r--config/locales/simple_form.ast.yml1
-rw-r--r--config/locales/simple_form.be.yml1
-rw-r--r--config/locales/simple_form.bg.yml1
-rw-r--r--config/locales/simple_form.ca.yml1
-rw-r--r--config/locales/simple_form.ckb.yml1
-rw-r--r--config/locales/simple_form.co.yml1
-rw-r--r--config/locales/simple_form.cs.yml1
-rw-r--r--config/locales/simple_form.cy.yml1
-rw-r--r--config/locales/simple_form.da.yml1
-rw-r--r--config/locales/simple_form.de.yml1
-rw-r--r--config/locales/simple_form.el.yml1
-rw-r--r--config/locales/simple_form.en-GB.yml1
-rw-r--r--config/locales/simple_form.en.yml2
-rw-r--r--config/locales/simple_form.eo.yml1
-rw-r--r--config/locales/simple_form.es-AR.yml1
-rw-r--r--config/locales/simple_form.es-MX.yml1
-rw-r--r--config/locales/simple_form.es.yml1
-rw-r--r--config/locales/simple_form.et.yml1
-rw-r--r--config/locales/simple_form.eu.yml1
-rw-r--r--config/locales/simple_form.fa.yml1
-rw-r--r--config/locales/simple_form.fi.yml1
-rw-r--r--config/locales/simple_form.fo.yml1
-rw-r--r--config/locales/simple_form.fr-QC.yml1
-rw-r--r--config/locales/simple_form.fr.yml1
-rw-r--r--config/locales/simple_form.fy.yml1
-rw-r--r--config/locales/simple_form.gd.yml1
-rw-r--r--config/locales/simple_form.gl.yml1
-rw-r--r--config/locales/simple_form.he.yml1
-rw-r--r--config/locales/simple_form.hu.yml1
-rw-r--r--config/locales/simple_form.hy.yml1
-rw-r--r--config/locales/simple_form.id.yml1
-rw-r--r--config/locales/simple_form.io.yml1
-rw-r--r--config/locales/simple_form.is.yml1
-rw-r--r--config/locales/simple_form.it.yml1
-rw-r--r--config/locales/simple_form.ja.yml1
-rw-r--r--config/locales/simple_form.kab.yml1
-rw-r--r--config/locales/simple_form.ko.yml1
-rw-r--r--config/locales/simple_form.ku.yml1
-rw-r--r--config/locales/simple_form.lv.yml1
-rw-r--r--config/locales/simple_form.my.yml1
-rw-r--r--config/locales/simple_form.nl.yml1
-rw-r--r--config/locales/simple_form.nn.yml1
-rw-r--r--config/locales/simple_form.no.yml1
-rw-r--r--config/locales/simple_form.oc.yml1
-rw-r--r--config/locales/simple_form.pl.yml1
-rw-r--r--config/locales/simple_form.pt-BR.yml1
-rw-r--r--config/locales/simple_form.pt-PT.yml1
-rw-r--r--config/locales/simple_form.ro.yml1
-rw-r--r--config/locales/simple_form.ru.yml1
-rw-r--r--config/locales/simple_form.sc.yml1
-rw-r--r--config/locales/simple_form.sco.yml1
-rw-r--r--config/locales/simple_form.si.yml1
-rw-r--r--config/locales/simple_form.sk.yml1
-rw-r--r--config/locales/simple_form.sl.yml1
-rw-r--r--config/locales/simple_form.sq.yml1
-rw-r--r--config/locales/simple_form.sr-Latn.yml1
-rw-r--r--config/locales/simple_form.sr.yml1
-rw-r--r--config/locales/simple_form.sv.yml1
-rw-r--r--config/locales/simple_form.th.yml1
-rw-r--r--config/locales/simple_form.tr.yml1
-rw-r--r--config/locales/simple_form.uk.yml1
-rw-r--r--config/locales/simple_form.vi.yml1
-rw-r--r--config/locales/simple_form.zh-CN.yml1
-rw-r--r--config/locales/simple_form.zh-HK.yml1
-rw-r--r--config/locales/simple_form.zh-TW.yml1
-rw-r--r--config/routes.rb30
-rw-r--r--jest.config.js2
-rw-r--r--lib/mastodon/accounts_cli.rb10
-rw-r--r--package.json6
-rw-r--r--spec/controllers/api/v1/admin/trends/links/preview_card_providers_controller_spec.rb68
-rw-r--r--spec/controllers/api/v1/admin/trends/links_controller_spec.rb49
-rw-r--r--spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb49
-rw-r--r--spec/controllers/api/v1/admin/trends/tags_controller_spec.rb49
-rw-r--r--spec/mailers/notification_mailer_spec.rb10
-rw-r--r--spec/models/account_filter_spec.rb19
-rw-r--r--spec/services/reblog_service_spec.rb22
-rw-r--r--yarn.lock34
131 files changed, 1174 insertions, 579 deletions
diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml
index 6a1cacb3f..1c4958550 100644
--- a/.github/workflows/test-js.yml
+++ b/.github/workflows/test-js.yml
@@ -9,6 +9,8 @@ on:
       - '.nvmrc'
       - '**/*.js'
       - '**/*.jsx'
+      - '**/*.ts'
+      - '**/*.tsx'
       - '**/*.snap'
       - '.github/workflows/test-js.yml'
 
@@ -19,6 +21,8 @@ on:
       - '.nvmrc'
       - '**/*.js'
       - '**/*.jsx'
+      - '**/*.ts'
+      - '**/*.tsx'
       - '**/*.snap'
       - '.github/workflows/test-js.yml'
 
diff --git a/Gemfile b/Gemfile
index cc94c878a..3c4ad3555 100644
--- a/Gemfile
+++ b/Gemfile
@@ -120,7 +120,7 @@ end
 group :test do
   gem 'capybara', '~> 3.39'
   gem 'climate_control'
-  gem 'faker', '~> 3.1'
+  gem 'faker', '~> 3.2'
   gem 'json-schema', '~> 3.0'
   gem 'rack-test', '~> 2.1'
   gem 'rails-controller-testing', '~> 1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 6ce9afdf7..2a67abf27 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -243,7 +243,7 @@ GEM
       tzinfo
     excon (0.95.0)
     fabrication (2.30.0)
-    faker (3.1.1)
+    faker (3.2.0)
       i18n (>= 1.8.11, < 2)
     faraday (1.10.3)
       faraday-em_http (~> 1.0)
@@ -348,19 +348,19 @@ GEM
     ipaddress (0.8.3)
     jmespath (1.6.2)
     json (2.6.3)
-    json-canonicalization (0.3.0)
+    json-canonicalization (0.3.1)
     json-jwt (1.15.3)
       activesupport (>= 4.2)
       aes_key_wrap
       bindata
       httpclient
-    json-ld (3.2.3)
+    json-ld (3.2.4)
       htmlentities (~> 4.3)
       json-canonicalization (~> 0.3)
       link_header (~> 0.0, >= 0.0.8)
       multi_json (~> 1.15)
-      rack (~> 2.2)
-      rdf (~> 3.2, >= 3.2.9)
+      rack (>= 2.2, < 4)
+      rdf (~> 3.2, >= 3.2.10)
     json-ld-preloaded (3.2.2)
       json-ld (~> 3.2)
       rdf (~> 3.2)
@@ -479,7 +479,7 @@ GEM
     openssl-signature_algorithm (1.3.0)
       openssl (> 2.0)
     orm_adapter (0.5.0)
-    ox (2.14.14)
+    ox (2.14.16)
     parallel (1.22.1)
     parser (3.2.2.0)
       ast (~> 2.4.1)
@@ -487,7 +487,7 @@ GEM
     pastel (0.8.0)
       tty-color (~> 0.5)
     pg (1.4.6)
-    pghero (3.3.1)
+    pghero (3.3.2)
       activerecord (>= 6)
     pkg-config (1.5.1)
     posix-spawn (0.3.15)
@@ -557,7 +557,7 @@ GEM
       thor (~> 1.0)
     rainbow (3.1.1)
     rake (13.0.6)
-    rdf (3.2.9)
+    rdf (3.2.10)
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.5.1)
       rdf (~> 3.2)
@@ -799,7 +799,7 @@ DEPENDENCIES
   dotenv-rails (~> 2.8)
   ed25519 (~> 1.3)
   fabrication (~> 2.30)
-  faker (~> 3.1)
+  faker (~> 3.2)
   fast_blank (~> 1.0)
   fastimage
   fog-core (<= 2.4.0)
diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb
new file mode 100644
index 000000000..5d9fcc82c
--- /dev/null
+++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseController
+  include Authorization
+
+  LIMIT = 100
+
+  before_action -> { authorize_if_got_token! :'admin:read' }, only: :index
+  before_action -> { authorize_if_got_token! :'admin:write' }, except: :index
+  before_action :set_providers, only: :index
+
+  after_action :verify_authorized
+  after_action :insert_pagination_headers, only: :index
+
+  PAGINATION_PARAMS = %i(limit).freeze
+
+  def index
+    authorize :preview_card_provider, :index?
+
+    render json: @providers, each_serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer
+  end
+
+  def approve
+    authorize :preview_card_provider, :review?
+
+    provider = PreviewCardProvider.find(params[:id])
+    provider.update(trendable: true, reviewed_at: Time.now.utc)
+    render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer
+  end
+
+  def reject
+    authorize :preview_card_provider, :review?
+
+    provider = PreviewCardProvider.find(params[:id])
+    provider.update(trendable: false, reviewed_at: Time.now.utc)
+    render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer
+  end
+
+  private
+
+  def set_providers
+    @providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def next_path
+    api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue?
+  end
+
+  def prev_path
+    api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
+  end
+
+  def pagination_max_id
+    @providers.last.id
+  end
+
+  def pagination_since_id
+    @providers.first.id
+  end
+
+  def records_continue?
+    @providers.size == limit_param(LIMIT)
+  end
+
+  def pagination_params(core_params)
+    params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
+  end
+end
diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb
index cc6388980..7f4ca4828 100644
--- a/app/controllers/api/v1/admin/trends/links_controller.rb
+++ b/app/controllers/api/v1/admin/trends/links_controller.rb
@@ -1,7 +1,36 @@
 # frozen_string_literal: true
 
 class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController
-  before_action -> { authorize_if_got_token! :'admin:read' }
+  include Authorization
+
+  before_action -> { authorize_if_got_token! :'admin:read' }, only: :index
+  before_action -> { authorize_if_got_token! :'admin:write' }, except: :index
+
+  after_action :verify_authorized, except: :index
+
+  def index
+    if current_user&.can?(:manage_taxonomies)
+      render json: @links, each_serializer: REST::Admin::Trends::LinkSerializer
+    else
+      super
+    end
+  end
+
+  def approve
+    authorize :preview_card, :review?
+
+    link = PreviewCard.find(params[:id])
+    link.update(trendable: true)
+    render json: link, serializer: REST::Admin::Trends::LinkSerializer
+  end
+
+  def reject
+    authorize :preview_card, :review?
+
+    link = PreviewCard.find(params[:id])
+    link.update(trendable: false)
+    render json: link, serializer: REST::Admin::Trends::LinkSerializer
+  end
 
   private
 
diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb
index c39f77363..34b6580df 100644
--- a/app/controllers/api/v1/admin/trends/statuses_controller.rb
+++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb
@@ -1,7 +1,36 @@
 # frozen_string_literal: true
 
 class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController
-  before_action -> { authorize_if_got_token! :'admin:read' }
+  include Authorization
+
+  before_action -> { authorize_if_got_token! :'admin:read' }, only: :index
+  before_action -> { authorize_if_got_token! :'admin:write' }, except: :index
+
+  after_action :verify_authorized, except: :index
+
+  def index
+    if current_user&.can?(:manage_taxonomies)
+      render json: @statuses, each_serializer: REST::Admin::Trends::StatusSerializer
+    else
+      super
+    end
+  end
+
+  def approve
+    authorize [:admin, :status], :review?
+
+    status = Status.find(params[:id])
+    status.update(trendable: true)
+    render json: status, serializer: REST::Admin::Trends::StatusSerializer
+  end
+
+  def reject
+    authorize [:admin, :status], :review?
+
+    status = Status.find(params[:id])
+    status.update(trendable: false)
+    render json: status, serializer: REST::Admin::Trends::StatusSerializer
+  end
 
   private
 
diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb
index e77df3021..2eeea9522 100644
--- a/app/controllers/api/v1/admin/trends/tags_controller.rb
+++ b/app/controllers/api/v1/admin/trends/tags_controller.rb
@@ -1,7 +1,12 @@
 # frozen_string_literal: true
 
 class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController
-  before_action -> { authorize_if_got_token! :'admin:read' }
+  include Authorization
+
+  before_action -> { authorize_if_got_token! :'admin:read' }, only: :index
+  before_action -> { authorize_if_got_token! :'admin:write' }, except: :index
+
+  after_action :verify_authorized, except: :index
 
   def index
     if current_user&.can?(:manage_taxonomies)
@@ -11,6 +16,22 @@ class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController
     end
   end
 
+  def approve
+    authorize :tag, :review?
+
+    tag = Tag.find(params[:id])
+    tag.update(trendable: true, reviewed_at: Time.now.utc)
+    render json: tag, serializer: REST::Admin::TagSerializer
+  end
+
+  def reject
+    authorize :tag, :review?
+
+    tag = Tag.find(params[:id])
+    tag.update(trendable: false, reviewed_at: Time.now.utc)
+    render json: tag, serializer: REST::Admin::TagSerializer
+  end
+
   private
 
   def enabled?
diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb
index db5a866f2..3ee35d141 100644
--- a/app/controllers/auth/setup_controller.rb
+++ b/app/controllers/auth/setup_controller.rb
@@ -11,15 +11,7 @@ class Auth::SetupController < ApplicationController
 
   skip_before_action :require_functional!
 
-  def show
-    flash.now[:notice] = begin
-      if @user.pending?
-        I18n.t('devise.registrations.signed_up_but_pending')
-      else
-        I18n.t('devise.registrations.signed_up_but_unconfirmed')
-      end
-    end
-  end
+  def show; end
 
   def update
     # This allows updating the e-mail without entering a password as is required
@@ -27,14 +19,13 @@ class Auth::SetupController < ApplicationController
     # that were not confirmed yet
 
     if @user.update(user_params)
-      redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions')
+      @user.resend_confirmation_instructions unless @user.confirmed?
+      redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent')
     else
       render :show
     end
   end
 
-  helper_method :missing_email?
-
   private
 
   def require_unconfirmed_or_pending!
@@ -53,10 +44,6 @@ class Auth::SetupController < ApplicationController
     params.require(:user).permit(:email)
   end
 
-  def missing_email?
-    truthy_param?(:missing_email)
-  end
-
   def set_pack
     use_pack 'auth'
   end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 2cac2de59..1228ce36c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -117,6 +117,10 @@ module ApplicationHelper
     content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
   end
 
+  def check_icon
+    content_tag(:svg, tag(:path, 'fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor')
+  end
+
   def visibility_icon(status)
     if status.public_visibility?
       fa_icon('globe', title: I18n.t('statuses.visibilities.public'))
diff --git a/app/javascript/flavours/glitch/components/animated_number.jsx b/app/javascript/flavours/glitch/components/animated_number.jsx
deleted file mode 100644
index dd21d97f0..000000000
--- a/app/javascript/flavours/glitch/components/animated_number.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ShortNumber from 'mastodon/components/short_number';
-import TransitionMotion from 'react-motion/lib/TransitionMotion';
-import spring from 'react-motion/lib/spring';
-import { reduceMotion } from 'flavours/glitch/initial_state';
-
-const obfuscatedCount = count => {
-  if (count < 0) {
-    return 0;
-  } else if (count <= 1) {
-    return count;
-  } else {
-    return '1+';
-  }
-};
-
-export default class AnimatedNumber extends React.PureComponent {
-
-  static propTypes = {
-    value: PropTypes.number.isRequired,
-    obfuscate: PropTypes.bool,
-  };
-
-  state = {
-    direction: 1,
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.value > this.props.value) {
-      this.setState({ direction: 1 });
-    } else if (nextProps.value < this.props.value) {
-      this.setState({ direction: -1 });
-    }
-  }
-
-  willEnter = () => {
-    const { direction } = this.state;
-
-    return { y: -1 * direction };
-  };
-
-  willLeave = () => {
-    const { direction } = this.state;
-
-    return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) };
-  };
-
-  render () {
-    const { value, obfuscate } = this.props;
-    const { direction } = this.state;
-
-    if (reduceMotion) {
-      return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />;
-    }
-
-    const styles = [{
-      key: `${value}`,
-      data: value,
-      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
-    }];
-
-    return (
-      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
-        {items => (
-          <span className='animated-number'>
-            {items.map(({ key, data, style }) => (
-              <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
-            ))}
-          </span>
-        )}
-      </TransitionMotion>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx
new file mode 100644
index 000000000..1673ff41b
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/animated_number.tsx
@@ -0,0 +1,58 @@
+import React, { useCallback, useState } from 'react';
+import ShortNumber from './short_number';
+import { TransitionMotion, spring } from 'react-motion';
+import { reduceMotion } from '../initial_state';
+
+const obfuscatedCount = (count: number) => {
+  if (count < 0) {
+    return 0;
+  } else if (count <= 1) {
+    return count;
+  } else {
+    return '1+';
+  }
+};
+
+type Props = {
+  value: number;
+  obfuscate?: boolean;
+}
+export const AnimatedNumber: React.FC<Props> = ({
+  value,
+  obfuscate,
+})=> {
+  const [previousValue, setPreviousValue] = useState(value);
+  const [direction, setDirection] = useState<1|-1>(1);
+
+  if (previousValue !== value) {
+    setPreviousValue(value);
+    setDirection(value > previousValue ? 1 : -1);
+  }
+
+  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
+  const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]);
+
+  if (reduceMotion) {
+    return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
+  }
+
+  const styles = [{
+    key: `${value}`,
+    data: value,
+    style: { y: spring(0, { damping: 35, stiffness: 400 }) },
+  }];
+
+  return (
+    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
+      {items => (
+        <span className='animated-number'>
+          {items.map(({ key, data, style }) => (
+            <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
+          ))}
+        </span>
+      )}
+    </TransitionMotion>
+  );
+};
+
+export default AnimatedNumber;
diff --git a/app/javascript/flavours/glitch/components/gifv.jsx b/app/javascript/flavours/glitch/components/gifv.jsx
deleted file mode 100644
index 1ce7e7c29..000000000
--- a/app/javascript/flavours/glitch/components/gifv.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export default class GIFV extends React.PureComponent {
-
-  static propTypes = {
-    src: PropTypes.string.isRequired,
-    alt: PropTypes.string,
-    lang: PropTypes.string,
-    width: PropTypes.number,
-    height: PropTypes.number,
-    onClick: PropTypes.func,
-  };
-
-  state = {
-    loading: true,
-  };
-
-  handleLoadedData = () => {
-    this.setState({ loading: false });
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.src !== this.props.src) {
-      this.setState({ loading: true });
-    }
-  }
-
-  handleClick = e => {
-    const { onClick } = this.props;
-
-    if (onClick) {
-      e.stopPropagation();
-      onClick();
-    }
-  };
-
-  render () {
-    const { src, width, height, alt, lang } = this.props;
-    const { loading } = this.state;
-
-    return (
-      <div className='gifv' style={{ position: 'relative' }}>
-        {loading && (
-          <canvas
-            width={width}
-            height={height}
-            role='button'
-            tabIndex={0}
-            aria-label={alt}
-            title={alt}
-            lang={lang}
-            onClick={this.handleClick}
-          />
-        )}
-
-        <video
-          src={src}
-          role='button'
-          tabIndex={0}
-          aria-label={alt}
-          title={alt}
-          lang={lang}
-          muted
-          loop
-          autoPlay
-          playsInline
-          onClick={this.handleClick}
-          onLoadedData={this.handleLoadedData}
-          style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
-        />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx
new file mode 100644
index 000000000..8968170c5
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/gifv.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback, useState } from 'react';
+
+type Props = {
+  src: string;
+  key: string;
+  alt?: string;
+  lang?: string;
+  width: number;
+  height: number;
+  onClick?: () => void;
+}
+
+export const GIFV: React.FC<Props> = ({
+  src,
+  alt,
+  lang,
+  width,
+  height,
+  onClick,
+})=> {
+  const [loading, setLoading] = useState(true);
+
+  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
+    setLoading(false);
+  }, [setLoading]);
+
+  const handleClick: React.MouseEventHandler = useCallback((e) => {
+    if (onClick) {
+      e.stopPropagation();
+      onClick();
+    }
+  }, [onClick]);
+
+  return (
+    <div className='gifv' style={{ position: 'relative' }}>
+      {loading && (
+        <canvas
+          width={width}
+          height={height}
+          role='button'
+          tabIndex={0}
+          aria-label={alt}
+          title={alt}
+          lang={lang}
+          onClick={handleClick}
+        />
+      )}
+
+      <video
+        src={src}
+        role='button'
+        tabIndex={0}
+        aria-label={alt}
+        title={alt}
+        lang={lang}
+        muted
+        loop
+        autoPlay
+        playsInline
+        onClick={handleClick}
+        onLoadedData={handleLoadedData}
+        style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
+      />
+    </div>
+  );
+};
+
+export default GIFV;
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
index 63a331086..06984f3ad 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
@@ -203,7 +203,7 @@ class Conversation extends ImmutablePureComponent {
               parseClick={this.parseClick}
               expanded={isExpanded}
               onExpandedToggle={this.handleShowMore}
-              collapsable
+              collapsible
               media={media}
             />
 
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx
index c220d761f..5d1160039 100644
--- a/app/javascript/flavours/glitch/features/status/index.jsx
+++ b/app/javascript/flavours/glitch/features/status/index.jsx
@@ -63,6 +63,7 @@ const messages = defineMessages({
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
+  statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}' },
   detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
@@ -161,13 +162,14 @@ const truncate = (str, num) => {
   }
 };
 
-const titleFromStatus = status => {
+const titleFromStatus = (intl, status) => {
   const displayName = status.getIn(['account', 'display_name']);
   const username = status.getIn(['account', 'username']);
-  const prefix = displayName.trim().length === 0 ? username : displayName;
+  const user = displayName.trim().length === 0 ? username : displayName;
   const text = status.get('search_index');
+  const attachmentCount = status.get('media_attachments').size;
 
-  return `${prefix}: "${truncate(text, 30)}"`;
+  return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
 };
 
 class Status extends ImmutablePureComponent {
@@ -710,7 +712,7 @@ class Status extends ImmutablePureComponent {
         </ScrollContainer>
 
         <Helmet>
-          <title>{titleFromStatus(status)}</title>
+          <title>{titleFromStatus(intl, status)}</title>
           <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
         </Helmet>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
index 2d49312e5..440a6ac4b 100644
--- a/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/filter_modal.jsx
@@ -131,4 +131,4 @@ class FilterModal extends ImmutablePureComponent {
 
 }
 
-export default connect(injectIntl(FilterModal));
+export default connect()(injectIntl(FilterModal));
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
index a5637d31c..78aee8dfe 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx
@@ -371,7 +371,7 @@ class FocalPointModal extends ImmutablePureComponent {
             {focals && (
               <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
                 {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
-                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />}
+                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
 
                 <div className='focal-point__preview'>
                   <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
index fd2bd43cf..6ca96b743 100644
--- a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
@@ -188,7 +188,7 @@ class MediaModal extends ImmutablePureComponent {
             src={image.get('url')}
             width={width}
             height={height}
-            key={image.get('preview_url')}
+            key={image.get('url')}
             alt={image.get('description')}
             lang={language}
             onClick={this.toggleNavigation}
diff --git a/app/javascript/flavours/glitch/packs/public.jsx b/app/javascript/flavours/glitch/packs/public.jsx
index 335a0710d..93b249bb4 100644
--- a/app/javascript/flavours/glitch/packs/public.jsx
+++ b/app/javascript/flavours/glitch/packs/public.jsx
@@ -2,6 +2,15 @@ import 'packs/public-path';
 import loadPolyfills from 'flavours/glitch/load_polyfills';
 import ready from 'flavours/glitch/ready';
 import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
+import axios from 'axios';
+import { throttle } from 'lodash';
+import { defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
+  passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' },
+  passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
+});
 
 function main() {
   const IntlMessageFormat = require('intl-messageformat').default;
@@ -9,7 +18,7 @@ function main() {
   const { delegate } = require('@rails/ujs');
   const emojify = require('flavours/glitch/features/emoji/emoji').default;
   const { getLocale } = require('locales');
-  const { messages } = getLocale();
+  const { localeData } = getLocale();
   const React = require('react');
   const ReactDOM = require('react-dom');
   const { createBrowserHistory } = require('history');
@@ -54,6 +63,11 @@ function main() {
       hour12: false,
     });
 
+    const formatMessage = ({ id, defaultMessage }, values) => {
+      const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
+      return messageFormat.format(values);
+    };
+
     [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
       content.innerHTML = emojify(content.innerHTML);
     });
@@ -73,7 +87,7 @@ function main() {
         date.getMonth() === today.getMonth() &&
         date.getFullYear() === today.getFullYear();
     };
-    const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale);
+    const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
 
     [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
       const datetime = new Date(content.getAttribute('datetime'));
@@ -99,7 +113,7 @@ function main() {
       const timeGiven = content.getAttribute('datetime').includes('T');
       content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
       content.textContent = timeAgoString({
-        formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values),
+        formatMessage,
         formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
       }, datetime, now, now.getFullYear(), timeGiven);
     });
@@ -128,17 +142,19 @@ function main() {
       scrollToDetailedStatus();
     }
 
-    delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
-      const password = document.getElementById('registration_user_password');
-      const confirmation = document.getElementById('registration_user_password_confirmation');
-      if (confirmation.value && confirmation.value.length > password.maxLength) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
-      } else if (password.value && password.value !== confirmation.value) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
+    delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
+      const username = document.getElementById('user_account_attributes_username');
+
+      if (username.value && username.value.length > 0) {
+        axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
+          username.setCustomValidity(formatMessage(messages.usernameTaken));
+        }).catch(() => {
+          username.setCustomValidity('');
+        });
       } else {
-        confirmation.setCustomValidity('');
+        username.setCustomValidity('');
       }
-    });
+    }, 500, { leading: false, trailing: true }));
 
     delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
       const password = document.getElementById('user_password');
@@ -146,9 +162,9 @@ function main() {
       if (!confirmation) return;
 
       if (confirmation.value && confirmation.value.length > password.maxLength) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
+        confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
       } else if (password.value && password.value !== confirmation.value) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
+        confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
       } else {
         confirmation.setCustomValidity('');
       }
@@ -162,10 +178,10 @@ function main() {
 
       if (statusEl.dataset.spoiler === 'expanded') {
         statusEl.dataset.spoiler = 'folded';
-        this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
+        this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
       } else {
         statusEl.dataset.spoiler = 'expanded';
-        this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
+        this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
       }
 
       return false;
@@ -173,7 +189,7 @@ function main() {
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
       const statusEl = spoilerLink.parentNode.parentNode;
-      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index bb44d1bac..f69e8f276 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -1118,3 +1118,89 @@ code {
     white-space: nowrap;
   }
 }
+
+.progress-tracker {
+  display: flex;
+  align-items: center;
+  padding-bottom: 30px;
+  margin-bottom: 30px;
+
+  li {
+    flex: 0 0 auto;
+    position: relative;
+  }
+
+  .separator {
+    height: 2px;
+    background: $ui-base-lighter-color;
+    flex: 1 1 auto;
+
+    &.completed {
+      background: $highlight-text-color;
+    }
+  }
+
+  .circle {
+    box-sizing: border-box;
+    position: relative;
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px solid $ui-base-lighter-color;
+    flex: 0 0 auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 16px;
+    }
+  }
+
+  .label {
+    position: absolute;
+    font-size: 14px;
+    font-weight: 500;
+    color: $secondary-text-color;
+    padding-top: 10px;
+    text-align: center;
+    width: 100px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+
+  li:first-child .label {
+    left: auto;
+    inset-inline-start: 0;
+    text-align: start;
+    transform: none;
+  }
+
+  li:last-child .label {
+    left: auto;
+    inset-inline-end: 0;
+    text-align: end;
+    transform: none;
+  }
+
+  .active .circle {
+    border-color: $highlight-text-color;
+
+    &::before {
+      content: '';
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background: $highlight-text-color;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+
+  .completed .circle {
+    border-color: $highlight-text-color;
+    background: $highlight-text-color;
+  }
+}
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap
index f8385357a..fbd44ecc5 100644
--- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap
@@ -3,6 +3,8 @@
 exports[`<AvatarOverlay renders a overlay avatar 1`] = `
 <div
   className="account__avatar-overlay"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
   style={
     {
       "height": 46,
@@ -15,8 +17,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
   >
     <div
       className="account__avatar"
-      onMouseEnter={[Function]}
-      onMouseLeave={[Function]}
       style={
         {
           "height": "36px",
@@ -35,8 +35,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
   >
     <div
       className="account__avatar"
-      onMouseEnter={[Function]}
-      onMouseLeave={[Function]}
       style={
         {
           "height": "24px",
diff --git a/app/javascript/mastodon/components/animated_number.jsx b/app/javascript/mastodon/components/animated_number.jsx
deleted file mode 100644
index ce688f04f..000000000
--- a/app/javascript/mastodon/components/animated_number.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ShortNumber from 'mastodon/components/short_number';
-import TransitionMotion from 'react-motion/lib/TransitionMotion';
-import spring from 'react-motion/lib/spring';
-import { reduceMotion } from 'mastodon/initial_state';
-
-const obfuscatedCount = count => {
-  if (count < 0) {
-    return 0;
-  } else if (count <= 1) {
-    return count;
-  } else {
-    return '1+';
-  }
-};
-
-export default class AnimatedNumber extends React.PureComponent {
-
-  static propTypes = {
-    value: PropTypes.number.isRequired,
-    obfuscate: PropTypes.bool,
-  };
-
-  state = {
-    direction: 1,
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.value > this.props.value) {
-      this.setState({ direction: 1 });
-    } else if (nextProps.value < this.props.value) {
-      this.setState({ direction: -1 });
-    }
-  }
-
-  willEnter = () => {
-    const { direction } = this.state;
-
-    return { y: -1 * direction };
-  };
-
-  willLeave = () => {
-    const { direction } = this.state;
-
-    return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) };
-  };
-
-  render () {
-    const { value, obfuscate } = this.props;
-    const { direction } = this.state;
-
-    if (reduceMotion) {
-      return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />;
-    }
-
-    const styles = [{
-      key: `${value}`,
-      data: value,
-      style: { y: spring(0, { damping: 35, stiffness: 400 }) },
-    }];
-
-    return (
-      <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
-        {items => (
-          <span className='animated-number'>
-            {items.map(({ key, data, style }) => (
-              <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
-            ))}
-          </span>
-        )}
-      </TransitionMotion>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx
new file mode 100644
index 000000000..1673ff41b
--- /dev/null
+++ b/app/javascript/mastodon/components/animated_number.tsx
@@ -0,0 +1,58 @@
+import React, { useCallback, useState } from 'react';
+import ShortNumber from './short_number';
+import { TransitionMotion, spring } from 'react-motion';
+import { reduceMotion } from '../initial_state';
+
+const obfuscatedCount = (count: number) => {
+  if (count < 0) {
+    return 0;
+  } else if (count <= 1) {
+    return count;
+  } else {
+    return '1+';
+  }
+};
+
+type Props = {
+  value: number;
+  obfuscate?: boolean;
+}
+export const AnimatedNumber: React.FC<Props> = ({
+  value,
+  obfuscate,
+})=> {
+  const [previousValue, setPreviousValue] = useState(value);
+  const [direction, setDirection] = useState<1|-1>(1);
+
+  if (previousValue !== value) {
+    setPreviousValue(value);
+    setDirection(value > previousValue ? 1 : -1);
+  }
+
+  const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
+  const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]);
+
+  if (reduceMotion) {
+    return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />;
+  }
+
+  const styles = [{
+    key: `${value}`,
+    data: value,
+    style: { y: spring(0, { damping: 35, stiffness: 400 }) },
+  }];
+
+  return (
+    <TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
+      {items => (
+        <span className='animated-number'>
+          {items.map(({ key, data, style }) => (
+            <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span>
+          ))}
+        </span>
+      )}
+    </TransitionMotion>
+  );
+};
+
+export default AnimatedNumber;
diff --git a/app/javascript/mastodon/components/avatar_overlay.jsx b/app/javascript/mastodon/components/avatar_overlay.jsx
deleted file mode 100644
index 034e8ba56..000000000
--- a/app/javascript/mastodon/components/avatar_overlay.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { autoPlayGif } from '../initial_state';
-import Avatar from './avatar';
-
-export default class AvatarOverlay extends React.PureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
-    friend: ImmutablePropTypes.map.isRequired,
-    animate: PropTypes.bool,
-    size: PropTypes.number,
-    baseSize: PropTypes.number,
-    overlaySize: PropTypes.number,
-  };
-
-  static defaultProps = {
-    animate: autoPlayGif,
-    size: 46,
-    baseSize: 36,
-    overlaySize: 24,
-  };
-
-  state = {
-    hovering: false,
-  };
-
-  handleMouseEnter = () => {
-    if (this.props.animate) return;
-    this.setState({ hovering: true });
-  };
-
-  handleMouseLeave = () => {
-    if (this.props.animate) return;
-    this.setState({ hovering: false });
-  };
-
-  render() {
-    const { account, friend, animate, size, baseSize, overlaySize } = this.props;
-    const { hovering } = this.state;
-
-    return (
-      <div className='account__avatar-overlay' style={{ width: size, height: size }}>
-        <div className='account__avatar-overlay-base'><Avatar animate={hovering || animate} account={account} size={baseSize} /></div>
-        <div className='account__avatar-overlay-overlay'><Avatar animate={hovering || animate} account={friend} size={overlaySize} /></div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx
new file mode 100644
index 000000000..5c65a928c
--- /dev/null
+++ b/app/javascript/mastodon/components/avatar_overlay.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import type { Account } from '../../types/resources';
+import { useHovering } from '../../hooks/useHovering';
+import { autoPlayGif } from '../initial_state';
+
+type Props = {
+  account: Account;
+  friend: Account;
+  size?: number;
+  baseSize?: number;
+  overlaySize?: number;
+};
+
+export const AvatarOverlay: React.FC<Props> = ({
+  account,
+  friend,
+  size = 46,
+  baseSize = 36,
+  overlaySize = 24,
+}) => {
+  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif);
+  const accountSrc = hovering ? account?.get('avatar') : account?.get('avatar_static');
+  const friendSrc = hovering ? friend?.get('avatar') : friend?.get('avatar_static');
+
+  return (
+    <div
+      className='account__avatar-overlay' style={{ width: size, height: size }}
+      onMouseEnter={handleMouseEnter}
+      onMouseLeave={handleMouseLeave}
+    >
+      <div className='account__avatar-overlay-base'>
+        <div
+          className='account__avatar'
+          style={{ width: `${baseSize}px`, height: `${baseSize}px` }}
+        >
+          {accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
+        </div>
+      </div>
+      <div className='account__avatar-overlay-overlay'>
+        <div
+          className='account__avatar'
+          style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
+        >
+          {friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default AvatarOverlay;
diff --git a/app/javascript/mastodon/components/gifv.jsx b/app/javascript/mastodon/components/gifv.jsx
deleted file mode 100644
index 1ce7e7c29..000000000
--- a/app/javascript/mastodon/components/gifv.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export default class GIFV extends React.PureComponent {
-
-  static propTypes = {
-    src: PropTypes.string.isRequired,
-    alt: PropTypes.string,
-    lang: PropTypes.string,
-    width: PropTypes.number,
-    height: PropTypes.number,
-    onClick: PropTypes.func,
-  };
-
-  state = {
-    loading: true,
-  };
-
-  handleLoadedData = () => {
-    this.setState({ loading: false });
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.src !== this.props.src) {
-      this.setState({ loading: true });
-    }
-  }
-
-  handleClick = e => {
-    const { onClick } = this.props;
-
-    if (onClick) {
-      e.stopPropagation();
-      onClick();
-    }
-  };
-
-  render () {
-    const { src, width, height, alt, lang } = this.props;
-    const { loading } = this.state;
-
-    return (
-      <div className='gifv' style={{ position: 'relative' }}>
-        {loading && (
-          <canvas
-            width={width}
-            height={height}
-            role='button'
-            tabIndex={0}
-            aria-label={alt}
-            title={alt}
-            lang={lang}
-            onClick={this.handleClick}
-          />
-        )}
-
-        <video
-          src={src}
-          role='button'
-          tabIndex={0}
-          aria-label={alt}
-          title={alt}
-          lang={lang}
-          muted
-          loop
-          autoPlay
-          playsInline
-          onClick={this.handleClick}
-          onLoadedData={this.handleLoadedData}
-          style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
-        />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx
new file mode 100644
index 000000000..8968170c5
--- /dev/null
+++ b/app/javascript/mastodon/components/gifv.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback, useState } from 'react';
+
+type Props = {
+  src: string;
+  key: string;
+  alt?: string;
+  lang?: string;
+  width: number;
+  height: number;
+  onClick?: () => void;
+}
+
+export const GIFV: React.FC<Props> = ({
+  src,
+  alt,
+  lang,
+  width,
+  height,
+  onClick,
+})=> {
+  const [loading, setLoading] = useState(true);
+
+  const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
+    setLoading(false);
+  }, [setLoading]);
+
+  const handleClick: React.MouseEventHandler = useCallback((e) => {
+    if (onClick) {
+      e.stopPropagation();
+      onClick();
+    }
+  }, [onClick]);
+
+  return (
+    <div className='gifv' style={{ position: 'relative' }}>
+      {loading && (
+        <canvas
+          width={width}
+          height={height}
+          role='button'
+          tabIndex={0}
+          aria-label={alt}
+          title={alt}
+          lang={lang}
+          onClick={handleClick}
+        />
+      )}
+
+      <video
+        src={src}
+        role='button'
+        tabIndex={0}
+        aria-label={alt}
+        title={alt}
+        lang={lang}
+        muted
+        loop
+        autoPlay
+        playsInline
+        onClick={handleClick}
+        onLoadedData={handleLoadedData}
+        style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
+      />
+    </div>
+  );
+};
+
+export default GIFV;
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 923dc892d..60a77a39c 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -541,7 +541,7 @@ class Status extends ImmutablePureComponent {
               expanded={!status.get('hidden')}
               onExpandedToggle={this.handleExpandedToggle}
               onTranslate={this.handleTranslate}
-              collapsable
+              collapsible
               onCollapsedToggle={this.handleCollapsedToggle}
             />
 
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index fb953b9dd..60f820bc5 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -65,7 +65,7 @@ class StatusContent extends React.PureComponent {
     onExpandedToggle: PropTypes.func,
     onTranslate: PropTypes.func,
     onClick: PropTypes.func,
-    collapsable: PropTypes.bool,
+    collapsible: PropTypes.bool,
     onCollapsedToggle: PropTypes.func,
     languages: ImmutablePropTypes.map,
     intl: PropTypes.object,
@@ -112,10 +112,10 @@ class StatusContent extends React.PureComponent {
     }
 
     if (status.get('collapsed', null) === null && onCollapsedToggle) {
-      const { collapsable, onClick } = this.props;
+      const { collapsible, onClick } = this.props;
 
       const collapsed =
-          collapsable
+          collapsible
           && onClick
           && node.clientHeight > MAX_HEIGHT
           && status.get('spoiler_text').length === 0;
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
index d0dbffe65..11f2790bf 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
@@ -165,7 +165,7 @@ class Conversation extends ImmutablePureComponent {
               onClick={this.handleClick}
               expanded={!lastStatus.get('hidden')}
               onExpandedToggle={this.handleShowMore}
-              collapsable
+              collapsible
             />
 
             {lastStatus.get('media_attachments').size > 0 && (
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index b547741f7..900b19c31 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -69,6 +69,7 @@ const messages = defineMessages({
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
+  statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}' },
   detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
@@ -166,13 +167,14 @@ const truncate = (str, num) => {
   }
 };
 
-const titleFromStatus = status => {
+const titleFromStatus = (intl, status) => {
   const displayName = status.getIn(['account', 'display_name']);
   const username = status.getIn(['account', 'username']);
-  const prefix = displayName.trim().length === 0 ? username : displayName;
+  const user = displayName.trim().length === 0 ? username : displayName;
   const text = status.get('search_index');
+  const attachmentCount = status.get('media_attachments').size;
 
-  return `${prefix}: "${truncate(text, 30)}"`;
+  return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
 };
 
 class Status extends ImmutablePureComponent {
@@ -670,7 +672,7 @@ class Status extends ImmutablePureComponent {
         </ScrollContainer>
 
         <Helmet>
-          <title>{titleFromStatus(status)}</title>
+          <title>{titleFromStatus(intl, status)}</title>
           <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
         </Helmet>
       </Column>
diff --git a/app/javascript/mastodon/features/ui/components/filter_modal.jsx b/app/javascript/mastodon/features/ui/components/filter_modal.jsx
index 32ebaf7b7..8d77fb3df 100644
--- a/app/javascript/mastodon/features/ui/components/filter_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/filter_modal.jsx
@@ -131,4 +131,4 @@ class FilterModal extends ImmutablePureComponent {
 
 }
 
-export default connect(injectIntl(FilterModal));
+export default connect()(injectIntl(FilterModal));
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
index 11c4c5237..2a1e4c8bb 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
@@ -383,7 +383,7 @@ class FocalPointModal extends ImmutablePureComponent {
             {focals && (
               <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
                 {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
-                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />}
+                {media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
 
                 <div className='focal-point__preview'>
                   <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.jsx b/app/javascript/mastodon/features/ui/components/media_modal.jsx
index 52bd75453..ec6ddc0e1 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx
@@ -186,7 +186,7 @@ class MediaModal extends ImmutablePureComponent {
             src={image.get('url')}
             width={width}
             height={height}
-            key={image.get('preview_url')}
+            key={image.get('url')}
             alt={image.get('description')}
             lang={language}
             onClick={this.toggleNavigation}
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 1351945eb..6d6683808 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -3733,6 +3733,10 @@
         "id": "status.show_less_all"
       },
       {
+        "defaultMessage": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}",
+        "id": "status.title.with_attachments"
+      },
+      {
         "defaultMessage": "Detailed conversation view",
         "id": "status.detailed_status"
       },
@@ -4354,5 +4358,22 @@
       }
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "That username is taken. Try another",
+        "id": "username.taken"
+      },
+      {
+        "defaultMessage": "Password confirmation exceeds the maximum password length",
+        "id": "password_confirmation.exceeds_maxlength"
+      },
+      {
+        "defaultMessage": "Password confirmation does not match",
+        "id": "password_confirmation.mismatching"
+      }
+    ],
+    "path": "app/javascript/packs/public.json"
   }
 ]
\ No newline at end of file
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index ae2d5a999..31fa3ca3a 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -443,6 +443,8 @@
   "notifications_permission_banner.enable": "Enable desktop notifications",
   "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
   "notifications_permission_banner.title": "Never miss a thing",
+  "password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length",
+  "password_confirmation.mismatching": "Password confirmation does not match",
   "picture_in_picture.restore": "Put it back",
   "poll.closed": "Closed",
   "poll.refresh": "Refresh",
@@ -598,6 +600,7 @@
   "status.show_more": "Show more",
   "status.show_more_all": "Show more for all",
   "status.show_original": "Show original",
+  "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}",
   "status.translate": "Translate",
   "status.translated_from_with": "Translated from {lang} using {provider}",
   "status.uncached_media_warning": "Not available",
@@ -650,6 +653,7 @@
   "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "upload_progress.processing": "Processing…",
+  "username.taken": "That username is taken. Try another",
   "video.close": "Close video",
   "video.download": "Download file",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index 0085563d2..886976823 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -597,6 +597,7 @@
   "status.show_more": "Mostrar más",
   "status.show_more_all": "Mostrar más para todo",
   "status.show_original": "Mostrar original",
+  "status.title.with_attachments": "{user} publicó {attachmentCount, plural, one {un archivo adjunto} other {{attachmentCount} archivos adjuntos}}",
   "status.translate": "Traducir",
   "status.translated_from_with": "Traducido del {lang} usando {provider}",
   "status.uncached_media_warning": "No disponible",
diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx
index ad6bf237f..606ddc3bf 100644
--- a/app/javascript/packs/public.jsx
+++ b/app/javascript/packs/public.jsx
@@ -4,6 +4,15 @@ import ready from '../mastodon/ready';
 import { start } from '../mastodon/common';
 import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
 import 'cocoon-js-vanilla';
+import axios from 'axios';
+import { throttle } from 'lodash';
+import { defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
+  passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' },
+  passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
+});
 
 start();
 
@@ -13,7 +22,7 @@ function main() {
   const { delegate } = require('@rails/ujs');
   const emojify = require('../mastodon/features/emoji/emoji').default;
   const { getLocale } = require('../mastodon/locales');
-  const { messages } = getLocale();
+  const { localeData } = getLocale();
   const React = require('react');
   const ReactDOM = require('react-dom');
   const { createBrowserHistory } = require('history');
@@ -58,6 +67,11 @@ function main() {
       hour12: false,
     });
 
+    const formatMessage = ({ id, defaultMessage }, values) => {
+      const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
+      return messageFormat.format(values);
+    };
+
     [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
       content.innerHTML = emojify(content.innerHTML);
     });
@@ -77,7 +91,7 @@ function main() {
         date.getMonth() === today.getMonth() &&
         date.getFullYear() === today.getFullYear();
     };
-    const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale);
+    const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
 
     [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
       const datetime = new Date(content.getAttribute('datetime'));
@@ -103,7 +117,7 @@ function main() {
       const timeGiven = content.getAttribute('datetime').includes('T');
       content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
       content.textContent = timeAgoString({
-        formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values),
+        formatMessage,
         formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
       }, datetime, now, now.getFullYear(), timeGiven);
     });
@@ -133,17 +147,19 @@ function main() {
       scrollToDetailedStatus();
     }
 
-    delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
-      const password = document.getElementById('registration_user_password');
-      const confirmation = document.getElementById('registration_user_password_confirmation');
-      if (confirmation.value && confirmation.value.length > password.maxLength) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
-      } else if (password.value && password.value !== confirmation.value) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
+    delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
+      const username = document.getElementById('user_account_attributes_username');
+
+      if (username.value && username.value.length > 0) {
+        axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
+          username.setCustomValidity(formatMessage(messages.usernameTaken));
+        }).catch(() => {
+          username.setCustomValidity('');
+        });
       } else {
-        confirmation.setCustomValidity('');
+        username.setCustomValidity('');
       }
-    });
+    }, 500, { leading: false, trailing: true }));
 
     delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
       const password = document.getElementById('user_password');
@@ -151,9 +167,9 @@ function main() {
       if (!confirmation) return;
 
       if (confirmation.value && confirmation.value.length > password.maxLength) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
+        confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
       } else if (password.value && password.value !== confirmation.value) {
-        confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
+        confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
       } else {
         confirmation.setCustomValidity('');
       }
@@ -167,10 +183,10 @@ function main() {
 
       if (statusEl.dataset.spoiler === 'expanded') {
         statusEl.dataset.spoiler = 'folded';
-        this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
+        this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
       } else {
         statusEl.dataset.spoiler = 'expanded';
-        this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
+        this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
       }
 
       return false;
@@ -178,7 +194,7 @@ function main() {
 
     [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
       const statusEl = spoilerLink.parentNode.parentNode;
-      const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
       spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
     });
   });
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 7d4bde5e9..dc52bcd87 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -1112,3 +1112,89 @@ code {
     white-space: nowrap;
   }
 }
+
+.progress-tracker {
+  display: flex;
+  align-items: center;
+  padding-bottom: 30px;
+  margin-bottom: 30px;
+
+  li {
+    flex: 0 0 auto;
+    position: relative;
+  }
+
+  .separator {
+    height: 2px;
+    background: $ui-base-lighter-color;
+    flex: 1 1 auto;
+
+    &.completed {
+      background: $highlight-text-color;
+    }
+  }
+
+  .circle {
+    box-sizing: border-box;
+    position: relative;
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px solid $ui-base-lighter-color;
+    flex: 0 0 auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 16px;
+    }
+  }
+
+  .label {
+    position: absolute;
+    font-size: 14px;
+    font-weight: 500;
+    color: $secondary-text-color;
+    padding-top: 10px;
+    text-align: center;
+    width: 100px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+
+  li:first-child .label {
+    left: auto;
+    inset-inline-start: 0;
+    text-align: start;
+    transform: none;
+  }
+
+  li:last-child .label {
+    left: auto;
+    inset-inline-end: 0;
+    text-align: end;
+    transform: none;
+  }
+
+  .active .circle {
+    border-color: $highlight-text-color;
+
+    &::before {
+      content: '';
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background: $highlight-text-color;
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+
+  .completed .circle {
+    border-color: $highlight-text-color;
+    background: $highlight-text-color;
+  }
+}
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index ab73826ab..c428fd30d 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -14,7 +14,7 @@ class NotificationMailer < ApplicationMailer
 
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
-      mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
+      mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
     end
   end
 
@@ -25,7 +25,7 @@ class NotificationMailer < ApplicationMailer
     return unless @me.user.functional?
 
     locale_for_account(@me) do
-      mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
+      mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
     end
   end
 
@@ -38,7 +38,7 @@ class NotificationMailer < ApplicationMailer
 
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
-      mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
+      mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
     end
   end
 
@@ -51,7 +51,7 @@ class NotificationMailer < ApplicationMailer
 
     locale_for_account(@me) do
       thread_by_conversation(@status.conversation)
-      mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
+      mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
     end
   end
 
@@ -62,7 +62,7 @@ class NotificationMailer < ApplicationMailer
     return unless @me.user.functional?
 
     locale_for_account(@me) do
-      mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
+      mail to: email_address_with_name(@me.user.email, @me.user.account.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
     end
   end
 
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 1666ea883..55d34e85c 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -55,7 +55,7 @@ class AccountFilter
     when 'by_domain'
       Account.where(domain: value.to_s.strip)
     when 'username'
-      Account.matches_username(value.to_s.strip)
+      Account.matches_username(value.to_s.strip.delete_prefix('@'))
     when 'display_name'
       Account.matches_display_name(value.to_s.strip)
     when 'email'
diff --git a/app/models/preview_card_provider.rb b/app/models/preview_card_provider.rb
index 1dd95fc91..9f5f6d3cb 100644
--- a/app/models/preview_card_provider.rb
+++ b/app/models/preview_card_provider.rb
@@ -18,6 +18,7 @@
 #
 
 class PreviewCardProvider < ApplicationRecord
+  include Paginable
   include DomainNormalizable
   include Attachmentable
 
diff --git a/app/serializers/rest/admin/trends/link_serializer.rb b/app/serializers/rest/admin/trends/link_serializer.rb
new file mode 100644
index 000000000..c93e6c178
--- /dev/null
+++ b/app/serializers/rest/admin/trends/link_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class REST::Admin::Trends::LinkSerializer < REST::Trends::LinkSerializer
+  attributes :id, :requires_review
+
+  def requires_review
+    object.requires_review?
+  end
+end
diff --git a/app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb b/app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb
new file mode 100644
index 000000000..fba0259fb
--- /dev/null
+++ b/app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class REST::Admin::Trends::Links::PreviewCardProviderSerializer < ActiveModel::Serializer
+  attributes :id, :domain, :trendable, :reviewed_at,
+             :requested_review_at, :requires_review
+
+  def requires_review
+    object.requires_review?
+  end
+end
diff --git a/app/serializers/rest/admin/trends/status_serializer.rb b/app/serializers/rest/admin/trends/status_serializer.rb
new file mode 100644
index 000000000..e46be30ab
--- /dev/null
+++ b/app/serializers/rest/admin/trends/status_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class REST::Admin::Trends::StatusSerializer < REST::StatusSerializer
+  attributes :requires_review
+
+  def requires_review
+    object.requires_review?
+  end
+end
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 069f370cf..ad9e6e3d6 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -7,6 +7,7 @@ class NotifyService < BaseService
     admin.report
     admin.sign_up
     update
+    poll
   ).freeze
 
   def call(recipient, type, activity)
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index 0d8fd800f..4df0f95d5 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -5,6 +5,8 @@
   = render partial: 'shared/og', locals: { description: description_for_sign_up }
 
 = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { novalidate: false }) do |f|
+  = render 'auth/shared/progress', stage: 'details'
+
   %h1.title= t('auth.sign_up.title', domain: site_hostname)
   %p.lead= t('auth.sign_up.preamble')
 
@@ -18,7 +20,7 @@
   .fields-group
     = f.simple_fields_for :account do |ff|
       = ff.input :display_name, wrapper: :with_label, label: false, required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.display_name'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.display_name') }
-      = ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false
+      = ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}"
     = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, hint: false
     = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, hint: false
     = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password' }, hint: false
@@ -26,9 +28,11 @@
     = f.input :website, as: :url, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.honeypot', label: 'Website'), autocomplete: 'off' }
 
   - if approved_registrations? && !@invite.present?
+    %p.lead= t('auth.sign_up.manual_review', domain: site_hostname)
+
     .fields-group
       = f.simple_fields_for :invite_request, resource.invite_request || resource.build_invite_request do |invite_request_fields|
-        = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text
+        = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text, label: false, hint: false
 
 
   = hidden_field_tag :accept, params[:accept]
diff --git a/app/views/auth/registrations/rules.html.haml b/app/views/auth/registrations/rules.html.haml
index 8e7a90cbe..aa16ab957 100644
--- a/app/views/auth/registrations/rules.html.haml
+++ b/app/views/auth/registrations/rules.html.haml
@@ -5,6 +5,8 @@
   = render partial: 'shared/og', locals: { description: description_for_sign_up }
 
 .simple_form
+  = render 'auth/shared/progress', stage: 'rules'
+
   %h1.title= t('auth.rules.title')
   %p.lead= t('auth.rules.preamble', domain: site_hostname)
 
diff --git a/app/views/auth/setup/show.html.haml b/app/views/auth/setup/show.html.haml
index 1a6611ceb..913b0c913 100644
--- a/app/views/auth/setup/show.html.haml
+++ b/app/views/auth/setup/show.html.haml
@@ -1,20 +1,22 @@
 - content_for :page_title do
   = t('auth.setup.title')
 
-- if missing_email?
-  = simple_form_for(@user, url: auth_setup_path) do |f|
-    = render 'shared/error_messages', object: @user
+= simple_form_for(@user, url: auth_setup_path) do |f|
+  = render 'auth/shared/progress', stage: 'confirm'
 
-    .fields-group
-      %p.hint= t('auth.setup.email_below_hint_html')
+  %h1.title= t('auth.setup.title')
+  %p.lead= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
 
-    .fields-group
-      = f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' }
+  = render 'shared/error_messages', object: @user
 
-    .actions
-      = f.submit t('admin.accounts.change_email.label'), class: 'button'
-- else
-  .simple_form
-    %p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
+  %p.lead
+    %strong= t('auth.setup.link_not_received')
+  %p.lead= t('auth.setup.email_below_hint_html')
+
+  .fields-group
+    = f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' }
+
+  .actions
+    = f.submit t('auth.resend_confirmation'), class: 'button'
 
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml
index f078e2f7e..757ef0a09 100644
--- a/app/views/auth/shared/_links.html.haml
+++ b/app/views/auth/shared/_links.html.haml
@@ -14,5 +14,5 @@
   - if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?)
     %li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
 
-  - if user_signed_in? && controller_name != 'setup'
+  - if user_signed_in?
     %li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }
diff --git a/app/views/auth/shared/_progress.html.haml b/app/views/auth/shared/_progress.html.haml
new file mode 100644
index 000000000..578f62fa9
--- /dev/null
+++ b/app/views/auth/shared/_progress.html.haml
@@ -0,0 +1,25 @@
+- progress_index = { rules: 0, details: 1, confirm: 2 }[stage.to_sym]
+
+%ol.progress-tracker
+  %li{ class: progress_index.positive? ? 'completed' : 'active' }
+    .circle
+      - if progress_index.positive?
+        = check_icon
+    .label= t('auth.progress.rules')
+  %li.separator{ class: progress_index.positive? ? 'completed' : nil }
+  %li{ class: [progress_index > 1 && 'completed', progress_index == 1 && 'active'] }
+    .circle
+      - if progress_index > 1
+        = check_icon
+    .label= t('auth.progress.details')
+  %li.separator{ class: progress_index > 1 ? 'completed' : nil }
+  %li{ class: [progress_index > 2 && 'completed', progress_index == 2 && 'active'] }
+    .circle
+      - if progress_index > 2
+        = check_icon
+    .label= t('auth.progress.confirm')
+  - if approved_registrations?
+    %li.separator{ class: progress_index > 2 ? 'completed' : nil }
+    %li
+      .circle
+      .label= t('auth.progress.review')
diff --git a/config/locales/en.yml b/config/locales/en.yml
index fa7fb6be0..e5b30d7e3 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -125,8 +125,8 @@ en:
       removed_header_msg: Successfully removed %{username}'s header image
       resend_confirmation:
         already_confirmed: This user is already confirmed
-        send: Resend confirmation email
-        success: Confirmation email successfully sent!
+        send: Resend confirmation link
+        success: Confirmation link successfully sent!
       reset: Reset
       reset_password: Reset password
       resubscribe: Resubscribe
@@ -988,7 +988,7 @@ en:
       prefix_invited_by_user: "@%{name} invites you to join this server of Mastodon!"
       prefix_sign_up: Sign up on Mastodon today!
       suffix: With an account, you will be able to follow people, post updates and exchange messages with users from any Mastodon server and more!
-    didnt_get_confirmation: Didn't receive confirmation instructions?
+    didnt_get_confirmation: Didn't receive a confirmation link?
     dont_have_your_security_key: Don't have your security key?
     forgot_password: Forgot your password?
     invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
@@ -1001,12 +1001,17 @@ en:
     migrate_account_html: If you wish to redirect this account to a different one, you can <a href="%{path}">configure it here</a>.
     or_log_in_with: Or log in with
     privacy_policy_agreement_html: I have read and agree to the <a href="%{privacy_policy_path}" target="_blank">privacy policy</a>
+    progress:
+      confirm: Confirm e-mail
+      details: Your details
+      review: Our review
+      rules: Accept rules
     providers:
       cas: CAS
       saml: SAML
     register: Sign up
     registration_closed: "%{instance} is not accepting new members"
-    resend_confirmation: Resend confirmation instructions
+    resend_confirmation: Resend confirmation link
     reset_password: Reset password
     rules:
       accept: Accept
@@ -1016,13 +1021,16 @@ en:
     security: Security
     set_new_password: Set new password
     setup:
-      email_below_hint_html: If the below e-mail address is incorrect, you can change it here and receive a new confirmation e-mail.
-      email_settings_hint_html: The confirmation e-mail was sent to %{email}. If that e-mail address is not correct, you can change it in account settings.
-      title: Setup
+      email_below_hint_html: Check your spam folder, or request another one. You can correct your e-mail address if it's wrong.
+      email_settings_hint_html: Click the link we sent you to verify %{email}. We'll wait right here.
+      link_not_received: Didn't get a link?
+      new_confirmation_instructions_sent: You will receive a new e-mail with the confirmation link in a few minutes!
+      title: Check your inbox
     sign_in:
       preamble_html: Sign in with your <strong>%{domain}</strong> credentials. If your account is hosted on a different server, you will not be able to log in here.
       title: Sign in to %{domain}
     sign_up:
+      manual_review: Sign-ups on %{domain} go through manual review by our moderators. To help us process your registration, write a bit about yourself and why you want an account on %{domain}.
       preamble: With an account on this Mastodon server, you'll be able to follow any other person on the network, regardless of where their account is hosted.
       title: Let's get you set up on %{domain}.
     status:
diff --git a/config/locales/simple_form.an.yml b/config/locales/simple_form.an.yml
index 10671134d..4d008fa42 100644
--- a/config/locales/simple_form.an.yml
+++ b/config/locales/simple_form.an.yml
@@ -59,7 +59,6 @@ an:
         setting_show_application: L'aplicación que utiliza vusté pa publicar publicacions s'amostrará en a vista detallada d'as suyas publicacions
         setting_use_blurhash: Los gradientes se basan en as colors d'as imachens amagadas pero fendo borrosos los detalles
         setting_use_pending_items: Amagar nuevos estaus dezaga d'un clic en cuenta de desplazar automaticament lo feed
-        username: Lo tuyo nombre d'usuario será solo en %{domain}
         whole_word: Quan la parola clau u frase ye nomás alfanumerica, nomás será aplicau si concuerda con tota la parola
       domain_allow:
         domain: Este dominio podrá obtener datos d'este servidor y los datos dentrants serán procesaus y archivados
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index 61befb9e6..449af0f0a 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -59,7 +59,6 @@ ar:
         setting_show_application: سيُعرَض اسم التطبيق الذي تستخدمه عند النشر في العرض المفصّل لمنشوراتك
         setting_use_blurhash: الألوان التدرّجية مبنية على ألوان المرئيات المخفية ولكنها تحجب كافة التفاصيل
         setting_use_pending_items: إخفاء تحديثات الخط وراء نقرة بدلًا مِن التمرير التلقائي للتدفق
-        username: اسم المستخدم الخاص بك سوف يكون فريدا مِن نوعه على %{domain}
         whole_word: إذا كانت الكلمة أو العبارة مكونة من أرقام وحروف فقط سوف يتم تطبيقها فقط عند مطابقة الكلمة ككل
       domain_allow:
         domain: سيكون بإمكان هذا النطاق جلب البيانات من هذا الخادم ومعالجة وتخزين البيانات الواردة منه
diff --git a/config/locales/simple_form.ast.yml b/config/locales/simple_form.ast.yml
index 9e97d51f2..1b159e2dc 100644
--- a/config/locales/simple_form.ast.yml
+++ b/config/locales/simple_form.ast.yml
@@ -35,7 +35,6 @@ ast:
         setting_noindex: Afeuta al perfil públicu ya a les páxines de los artículos
         setting_show_application: L'aplicación qu'uses pa espublizar apaez na vista detallada de los tos artículos
         setting_use_blurhash: Los dilíos básense nos colores del conteníu multimedia anubríu mas desenfonca los detalles
-        username: 'El nome d''usuariu va ser únicu en: %{domain}'
       featured_tag:
         name: 'Equí tán dalgunes de les etiquetes qu''usesti apocayá:'
       form_admin_settings:
diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml
index 4a2c532ce..f4e46fa0f 100644
--- a/config/locales/simple_form.be.yml
+++ b/config/locales/simple_form.be.yml
@@ -59,7 +59,6 @@ be:
         setting_show_application: Праграма, праз якую вы ствараеце допісы, будзе паказвацца ў падрабязнасцях пра допісы
         setting_use_blurhash: Градыенты заснаваны на колерах схаваных выяў, але размываюць дэталі
         setting_use_pending_items: Схаваць абнаўленні стужкі за клікам замест аўтаматычнага пракручвання стужкі
-        username: Ваша імя карыстальніка будзе ўнікальным на %{domain}
         whole_word: Калі ключавое слова ці фраза складаецца толькі з літар і лічбаў, яно будзе ўжытае толькі калі супадае з усім словам
       domain_allow:
         domain: Гэты дамен зможа атрымліваць даныя з гэтага сервера. Даныя з гэтага дамену будуць апрацаваныя ды захаваныя
diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml
index e84edc50d..b611d65fd 100644
--- a/config/locales/simple_form.bg.yml
+++ b/config/locales/simple_form.bg.yml
@@ -59,7 +59,6 @@ bg:
         setting_show_application: Приложението, което ползвате за публикуване, ще се показва в подробностите на публикацията ви
         setting_use_blurhash: Преливането е въз основа на цветовете на скритите визуализации, но се замъгляват подробностите
         setting_use_pending_items: Да се показват обновявания на часовата ос само след щракване вместо автоматично превъртане на инфоканала
-        username: Вашето потребителско име ще е неповторимо в %{domain}
         whole_word: Ако ключовата дума или фраза е само буквеноцифрена, то ще се приложи само, ако съвпадне с цялата дума
       domain_allow:
         domain: Домейнът ще може да извлича данни от този сървър и входящите данни от него ще се обработят и съхранят
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index bcabca034..821600f5d 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -59,7 +59,6 @@ ca:
         setting_show_application: L'aplicació que fas servir per a publicar es mostrarà a la vista detallada dels teus tuts
         setting_use_blurhash: Els degradats es basen en els colors de les imatges ocultes, però n'enfosqueixen els detalls
         setting_use_pending_items: Amaga les actualitzacions de la línia de temps després de fer un clic, en lloc de desplaçar-les automàticament
-        username: El teu nom d'usuari serà únic a %{domain}
         whole_word: Quan la paraula clau o la frase sigui només alfanumèrica, s'aplicarà si coincideix amb la paraula sencera
       domain_allow:
         domain: Aquest domini podrà obtenir dades d’aquest servidor i les dades entrants d’aquests seran processades i emmagatzemades
diff --git a/config/locales/simple_form.ckb.yml b/config/locales/simple_form.ckb.yml
index 9ce9ac065..e52345020 100644
--- a/config/locales/simple_form.ckb.yml
+++ b/config/locales/simple_form.ckb.yml
@@ -51,7 +51,6 @@ ckb:
         setting_show_application: بەرنامەیەک کە بە یارمەتیت توت دەکەیت، لە دیمەنی وردی توتەکان پیشان دەدرێت
         setting_use_blurhash: سێبەرەکان لە سەر بنەمای ڕەنگەکانی بەکارهاتوو لە وێنە داشاراوەکان دروست دەبن بەڵام وردەزانیاری وێنە تێیدا ڕوون نییە
         setting_use_pending_items: لەجیاتی ئەوەی بە خۆکارانە کێشان هەبێت لە نووسراوەکان بە کرتەیەک بەڕۆژبوونی پێرستی نووسراوەکان بشارەوە
-        username: ناوی بەکارهێنەری ئێوە لەسەر %{domain} یەکتا دەبێت
         whole_word: کاتێک کلیل‌وشە بریتییە لە ژمارە و پیت، تنەها کاتێک پەیدا دەبێت کە لەگەڵ گشتی وشە لە نێو دەقەکە هاوئاهەنگ بێت، نە تەنها لەگەڵ بەشێک لە وشە
       domain_allow:
         domain: ئەم دۆمەینە دەتوانێت دراوە لە ئەم ڕاژە وەربگرێت و دراوەی ئەم دۆمەینە لێرە ڕێکدەخرین و پاشکەوت دەکرێن
diff --git a/config/locales/simple_form.co.yml b/config/locales/simple_form.co.yml
index 79e5837d4..b03ff4a09 100644
--- a/config/locales/simple_form.co.yml
+++ b/config/locales/simple_form.co.yml
@@ -49,7 +49,6 @@ co:
         setting_show_application: L'applicazione chì voi utilizate per mandà statuti sarà affissata indè a vista ditagliata di quelli
         setting_use_blurhash: I digradati blurhash sò basati nant'à i culori di u ritrattu piattatu ma senza i ditagli
         setting_use_pending_items: Clicchi per messe à ghjornu i statuti invece di fà sfilà a linea autumaticamente
-        username: U vostru cugnome sarà unicu nant'à %{domain}
         whole_word: Quandu a parolla o a frasa sana hè alfanumerica, sarà applicata solu s'ella currisponde à a parolla sana
       domain_allow:
         domain: Stu duminiu puderà ricuperà i dati di stu servore è i dati ch'affaccanu da quallà saranu trattati è cunservati
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 7025c6385..f253c82b9 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -59,7 +59,6 @@ cs:
         setting_show_application: Aplikace, kterou používáte k odeslání příspěvků, bude zobrazena jejich detailním zobrazení
         setting_use_blurhash: Gradienty jsou založeny na barvách skryté grafiky, ale zakrývají jakékoliv detaily
         setting_use_pending_items: Aktualizovat časovou osu až po kliknutí namísto automatického rolování kanálu
-        username: Vaše uživatelské jméno bude na serveru %{domain} unikátní
         whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikován pouze, pokud se shoduje s celým slovem
       domain_allow:
         domain: Tato doména bude moci stahovat data z tohoto serveru a příchozí data z ní budou zpracována a uložena
diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml
index d965f6b71..f482e6ea9 100644
--- a/config/locales/simple_form.cy.yml
+++ b/config/locales/simple_form.cy.yml
@@ -59,7 +59,6 @@ cy:
         setting_show_application: Bydd y cymhwysiad a ddefnyddiwch i bostio yn cael ei arddangos yng ngolwg fanwl eich postiadau
         setting_use_blurhash: Mae graddiannau wedi'u seilio ar liwiau'r delweddau cudd ond maen nhw'n cuddio unrhyw fanylion
         setting_use_pending_items: Cuddio diweddariadau llinell amser y tu ôl i glic yn lle sgrolio'n awtomatig
-        username: Bydd eich enw defnyddiwr yn unigryw ar %{domain}
         whole_word: Os yw'r allweddair neu'r ymadrodd yn alffaniwmerig yn unig, mi fydd ond yn cael ei osod os yw'n cyfateb a'r gair cyfan
       domain_allow:
         domain: Bydd y parth hwn yn gallu nôl data o'r gweinydd hwn a bydd data sy'n dod i mewn ohono yn cael ei brosesu a'i storio
diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml
index af2942742..501c33b1c 100644
--- a/config/locales/simple_form.da.yml
+++ b/config/locales/simple_form.da.yml
@@ -59,7 +59,6 @@ da:
         setting_show_application: Applikationen, hvormed der postes, vil fremgå af detailvisningen af dine indlæg
         setting_use_blurhash: Gradienter er baseret på de skjulte grafikelementers farver, men slører alle detaljer
         setting_use_pending_items: Skjul tidslinjeopdateringer bag et klik i stedet for brug af auto-feedrulning
-        username: Dit brugernavn vil være unikt på %{domain}
         whole_word: Ved rent alfanumeriske nøgleord/-sætning, forudsætter brugen matchning af hele ordet
       domain_allow:
         domain: Dette domæne vil kunne hente data, som efterfølgende behandles og gemmes, fra denne server
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 4a13cea7c..2b84def3d 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -59,7 +59,6 @@ de:
         setting_show_application: Die Anwendung die du nutzt wird in der detaillierten Ansicht deiner Beiträge angezeigt
         setting_use_blurhash: Die Farbverläufe basieren auf den Farben der verborgenen Medien, verschleiern aber jegliche Details
         setting_use_pending_items: Neue Beiträge hinter einem Klick verstecken, anstatt des automatischen Bildlaufs
-        username: Dein Profilname wird auf %{domain} einmalig sein
         whole_word: Wenn das Wort oder die Formulierung nur aus Buchstaben oder Zahlen besteht, tritt der Filter nur dann in Kraft, wenn er exakt dieser Zeichenfolge entspricht
       domain_allow:
         domain: Diese Domain kann Daten von diesem Server abrufen, und eingehende Daten werden verarbeitet und gespeichert
diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml
index 2788fd9b5..88a9840da 100644
--- a/config/locales/simple_form.el.yml
+++ b/config/locales/simple_form.el.yml
@@ -59,7 +59,6 @@ el:
         setting_show_application: Η εφαρμογή που χρησιμοποιείς για να στέλνεις τα τουτ σου θα εμφανίζεται στις αναλυτικές λεπτομέρειες τους
         setting_use_blurhash: Οι χρωματισμοί βασίζονται στα χρώματα του κρυμμένου πολυμέσου αλλά θολώνουν τις λεπτομέρειες
         setting_use_pending_items: Εμφάνιση ενημερώσεων ροής μετά από κλικ αντί για αυτόματη κύλισή τους
-        username: Το όνομα χρήστη σου θα είναι μοναδικό στο %{domain}
         whole_word: Όταν η λέξη ή η φράση κλειδί είναι μόνο αλφαριθμητική, θα εφαρμοστεί μόνο αν ταιριάζει με ολόκληρη τη λέξη
       domain_allow:
         domain: Ο τομέας αυτός θα επιτρέπεται να ανακτά δεδομένα από αυτό τον διακομιστή και τα εισερχόμενα δεδομένα θα επεξεργάζονται και θα αποθηκεύονται
diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml
index 27aa80c42..35b80fd07 100644
--- a/config/locales/simple_form.en-GB.yml
+++ b/config/locales/simple_form.en-GB.yml
@@ -59,7 +59,6 @@ en-GB:
         setting_show_application: The application you use to post will be displayed in the detailed view of your posts
         setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
         setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
-        username: Your username will be unique on %{domain}
         whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
       domain_allow:
         domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 96b0131ef..b646a15e2 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -59,7 +59,7 @@ en:
         setting_show_application: The application you use to post will be displayed in the detailed view of your posts
         setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
         setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
-        username: Your username will be unique on %{domain}
+        username: You can use letters, numbers, and underscores
         whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
       domain_allow:
         domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index d54248de0..0e0123af7 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -59,7 +59,6 @@ eo:
         setting_show_application: La aplikaĵo, kiun vi uzas por afiŝi, estos montrita en la detala vido de viaj afiŝoj
         setting_use_blurhash: Transirojn estas bazita sur la koloroj de la kaŝitaj aŭdovidaĵoj sed ne montri iun ajn detalon
         setting_use_pending_items: Kaŝi tempoliniajn ĝisdatigojn malantaŭ klako anstataŭ aŭtomate rulumi la fluon
-        username: Via uzantnomo estos unika en %{domain}
         whole_word: Kiam la vorto aŭ frazo estas nur litera aŭ cifera, ĝi estos uzata nur se ĝi kongruas kun la tuta vorto
       domain_allow:
         domain: Ĉi tiu domajno povos akiri datumon de ĉi tiu servilo kaj envenanta datumo estos prilaborita kaj konservita
diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml
index bcceccfb1..0d29d6612 100644
--- a/config/locales/simple_form.es-AR.yml
+++ b/config/locales/simple_form.es-AR.yml
@@ -59,7 +59,6 @@ es-AR:
         setting_show_application: La aplicación que usás para enviar mensajes se mostrará en la vista detallada de tus mensajes
         setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles
         setting_use_pending_items: Ocultar actualizaciones de la línea temporal detrás de un clic en lugar de desplazar automáticamente el flujo
-        username: Tu nombre de usuario será único en %{domain}
         whole_word: Cuando la palabra clave o frase es sólo alfanumérica, sólo será aplicado si coincide con toda la palabra
       domain_allow:
         domain: Este dominio podrá recolectar datos de este servidor, y los datos entrantes serán procesados y archivados
diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml
index 52d8974f2..efc4f0d80 100644
--- a/config/locales/simple_form.es-MX.yml
+++ b/config/locales/simple_form.es-MX.yml
@@ -59,7 +59,6 @@ es-MX:
         setting_show_application: La aplicación que utiliza usted para publicar toots se mostrará en la vista detallada de sus toots
         setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles
         setting_use_pending_items: Ocultar nuevos estados detrás de un clic en lugar de desplazar automáticamente el feed
-        username: Tu nombre de usuario será único en %{domain}
         whole_word: Cuando la palabra clave o frase es solo alfanumérica, solo será aplicado si concuerda con toda la palabra
       domain_allow:
         domain: Este dominio podrá obtener datos de este servidor y los datos entrantes serán procesados y archivados
diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml
index 593416aca..a2eafe617 100644
--- a/config/locales/simple_form.es.yml
+++ b/config/locales/simple_form.es.yml
@@ -59,7 +59,6 @@ es:
         setting_show_application: La aplicación que utiliza usted para publicar publicaciones se mostrará en la vista detallada de sus publicaciones
         setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles
         setting_use_pending_items: Ocultar nuevos estados detrás de un clic en lugar de desplazar automáticamente el feed
-        username: Tu nombre de usuario será único en %{domain}
         whole_word: Cuando la palabra clave o frase es solo alfanumérica, solo será aplicado si concuerda con toda la palabra
       domain_allow:
         domain: Este dominio podrá obtener datos de este servidor y los datos entrantes serán procesados y archivados
diff --git a/config/locales/simple_form.et.yml b/config/locales/simple_form.et.yml
index 83aa4159d..ce3bc8b96 100644
--- a/config/locales/simple_form.et.yml
+++ b/config/locales/simple_form.et.yml
@@ -59,7 +59,6 @@ et:
         setting_show_application: Postitamiseks kasutatud rakenduse infot kuvatakse postituse üksikasjavaates
         setting_use_blurhash: Värvid põhinevad peidetud visuaalidel, kuid hägustavad igasuguseid detaile
         setting_use_pending_items: Voo automaatse kerimise asemel peida ajajoone uuendused kliki taha
-        username: Sinu kasutajanimi on %{domain}-il unikaalne
         whole_word: Kui võtmesõna või fraas on ainult tähtnumbriline, rakendub see ainult siis, kui see kattub terve sõnaga
       domain_allow:
         domain: See domeen saab tõmmata andmeid sellelt serverilt ning sissetulevad andmed sellelt domeenilt töödeldakse ning salvestatakse
diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml
index 175e0c96d..303011447 100644
--- a/config/locales/simple_form.eu.yml
+++ b/config/locales/simple_form.eu.yml
@@ -59,7 +59,6 @@ eu:
         setting_show_application: Tootak bidaltzeko erabiltzen duzun aplikazioa zure tooten ikuspegi xehetsuan bistaratuko da
         setting_use_blurhash: Gradienteak ezkutatutakoaren koloreetan oinarritzen dira, baina xehetasunak ezkutatzen dituzte
         setting_use_pending_items: Ezkutatu denbora-lerroko eguneraketak klik baten atzean jarioa automatikoki korritu ordez
-        username: Zure erabiltzaile-izena bakana izango da %{domain} domeinuan
         whole_word: Hitz eta esaldi gakoa alfanumerikoa denean, hitz osoarekin bat datorrenean besterik ez da aplikatuko
       domain_allow:
         domain: Domeinu honek zerbitzari honetatik datuak hartu ahal izango ditu eta bertatik jasotako informazioa prozesatu eta gordeko da
diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml
index 18dd82790..d9db0c697 100644
--- a/config/locales/simple_form.fa.yml
+++ b/config/locales/simple_form.fa.yml
@@ -59,7 +59,6 @@ fa:
         setting_show_application: برنامه‌ای که به کمک آن فرسته می‌زنید، در جزئیات فرسته شما نمایش خواهد یافت
         setting_use_blurhash: سایه‌ها بر اساس رنگ‌های به‌کاررفته در تصویر پنهان‌شده ساخته می‌شوند ولی جزئیات تصویر در آن‌ها آشکار نیست
         setting_use_pending_items: به جای پیش‌رفتن خودکار در فهرست، به‌روزرسانی فهرست نوشته‌ها را پشت یک کلیک پنهان کن
-        username: نام کاربری شما روی %{domain} یکتا خواهد بود
         whole_word: اگر کلیدواژه فقط دارای حروف و اعداد باشد، تنها وقتی پیدا می‌شود که با کل یک واژه در متن منطبق باشد، نه با بخشی از یک واژه
       domain_allow:
         domain: این دامین خواهد توانست داده‌ها از این سرور را دریافت کند و داده‌های از این دامین در این‌جا پردازش و ذخیره خواهند شد
diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml
index b96cf0cd7..118054855 100644
--- a/config/locales/simple_form.fi.yml
+++ b/config/locales/simple_form.fi.yml
@@ -59,7 +59,6 @@ fi:
         setting_show_application: Viestittelyyn käyttämäsi sovellus näkyy viestiesi yksityiskohtaisessa näkymässä
         setting_use_blurhash: Liukuvärit perustuvat piilotettujen kuvien väreihin, mutta sumentavat yksityiskohdat
         setting_use_pending_items: Piilota aikajanan päivitykset napsautuksen taakse sen sijaan, että vierittäisi syötettä automaattisesti
-        username: Käyttäjänimesi tulee olemaan yksilöllinen %{domain}
         whole_word: Kun avainsana tai lause on vain aakkosnumeerinen, se otetaan käyttöön, jos se vastaa koko sanaa
       domain_allow:
         domain: Tämä verkkotunnus voi noutaa tietoja tältä palvelimelta ja sieltä saapuvat tiedot käsitellään ja tallennetaan
diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml
index 2713d1f89..d22851682 100644
--- a/config/locales/simple_form.fo.yml
+++ b/config/locales/simple_form.fo.yml
@@ -59,7 +59,6 @@ fo:
         setting_show_application: Nýtsluskipanin, sum tú brúkar at posta við, verður víst í nágreinligu vísingini av postum tínum
         setting_use_blurhash: Gradientar eru grundaðir á litirnar av fjaldu myndunum, men grugga allar smálutir
         setting_use_pending_items: Fjal tíðarlinjudagføringar aftan fyri eitt klikk heldur enn at skrulla tilføringina sjálvvirkandi
-        username: Brúkaranavnið hjá tær verður eindømi á %{domain}
         whole_word: Tá lyklaorðið ella frasan einans hevur bókstavir og tøl, so verður hon einans nýtt, um tú samsvarar við alt orðið
       domain_allow:
         domain: Økisnavnið kann heinta dátur frá hesum ambætaranum og inngangandi dátur frá honum verða viðgjørdar og goymdar
diff --git a/config/locales/simple_form.fr-QC.yml b/config/locales/simple_form.fr-QC.yml
index 541bc8be6..6517d3d8d 100644
--- a/config/locales/simple_form.fr-QC.yml
+++ b/config/locales/simple_form.fr-QC.yml
@@ -59,7 +59,6 @@ fr-QC:
         setting_show_application: Le nom de l’application que vous utilisez pour publier sera affichée dans la vue détaillée de vos messages
         setting_use_blurhash: Les dégradés sont basés sur les couleurs des images cachées mais n’en montrent pas les détails
         setting_use_pending_items: Cacher les mises à jour des fils d’actualités derrière un clic, au lieu de les afficher automatiquement
-        username: Votre nom d’utilisateur sera unique sur %{domain}
         whole_word: Si le mot-clé ou la phrase est alphanumérique, alors le filtre ne sera appliqué que s’il correspond au mot entier
       domain_allow:
         domain: Ce domaine pourra récupérer des données de ce serveur et les données entrantes seront traitées et stockées
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index d395b8517..d4697fa1e 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -59,7 +59,6 @@ fr:
         setting_show_application: Le nom de l’application que vous utilisez pour publier sera affichée dans la vue détaillée de vos messages
         setting_use_blurhash: Les dégradés sont basés sur les couleurs des images cachées mais n’en montrent pas les détails
         setting_use_pending_items: Cacher les mises à jour des fils d’actualités derrière un clic, au lieu de les afficher automatiquement
-        username: Votre identifiant sera unique sur %{domain}
         whole_word: Si le mot-clé ou la phrase est alphanumérique, alors le filtre ne sera appliqué que s’il correspond au mot entier
       domain_allow:
         domain: Ce domaine pourra récupérer des données de ce serveur et les données entrantes seront traitées et stockées
diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml
index be950189b..547a2a5ed 100644
--- a/config/locales/simple_form.fy.yml
+++ b/config/locales/simple_form.fy.yml
@@ -59,7 +59,6 @@ fy:
         setting_show_application: De tapassing dy’t jo brûke om berjochten te pleatsen, wurdt yn de detaillearre werjefte fan it berjocht toand
         setting_use_blurhash: Dizige kleuroergongen binne basearre op de kleuren fan de ferstoppe media, wêrmei elk detail ferdwynt
         setting_use_pending_items: De tiidline wurdt bywurke troch op it oantal nije items te klikken, yn stee fan dat dizze automatysk bywurke wurdt
-        username: Jo brûkersnamme is unyk op %{domain}
         whole_word: Wannear it trefwurd of part fan de sin alfanumeryk is, wurdt it allinnich filtere wannear’t it hiele wurd oerienkomt
       domain_allow:
         domain: Dit domein is yn steat om gegevens fan dizze server op te heljen, en ynkommende gegevens wurde ferwurke en bewarre
diff --git a/config/locales/simple_form.gd.yml b/config/locales/simple_form.gd.yml
index c86fc5a0c..a34865ff7 100644
--- a/config/locales/simple_form.gd.yml
+++ b/config/locales/simple_form.gd.yml
@@ -59,7 +59,6 @@ gd:
         setting_show_application: Chithear cò an aplacaid a chleachd thu airson post a sgrìobhadh ann an seallaidhean mionaideach nam postaichean agad
         setting_use_blurhash: Tha caiseadan stèidhichte air dathan nan nithean lèirsinneach a chaidh fhalach ach chan fhaicear am mion-fhiosrachadh
         setting_use_pending_items: Falaich ùrachaidhean na loidhne-ama air cùlaibh briogaidh seach a bhith a’ sgroladh nam postaichean gu fèin-obrachail
-        username: Bidh ainm-cleachdaiche àraidh agad air %{domain}
         whole_word: Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin
       domain_allow:
         domain: "’S urrainn dhan àrainn seo dàta fhaighinn on fhrithealaiche seo agus thèid an dàta a thig a-steach uaithe a phròiseasadh ’s a stòradh"
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
index c4a6da566..ec038f9b6 100644
--- a/config/locales/simple_form.gl.yml
+++ b/config/locales/simple_form.gl.yml
@@ -59,7 +59,6 @@ gl:
         setting_show_application: A aplicación que estás a utilizar para enviar publicacións mostrarase na vista detallada da publicación
         setting_use_blurhash: Os gradientes toman as cores da imaxe oculta pero esvaecendo tódolos detalles
         setting_use_pending_items: Agochar actualizacións da cronoloxía tras un click no lugar de desprazar automáticamente os comentarios
-        username: O teu nome de usuaria será único en %{domain}
         whole_word: Se a chave ou frase de paso é só alfanumérica, só se aplicará se concorda a palabra completa
       domain_allow:
         domain: Este dominio estará en disposición de obter datos desde este servidor e datos de entrada a el poderán ser procesados e gardados
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 8e6631e36..e540c3473 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -59,7 +59,6 @@ he:
         setting_show_application: היישום בו נעשה שימוש כדי לחצרץ יופיע בתצוגה המפורטת של החצרוץ
         setting_use_blurhash: הגראדיינטים מבוססים על תוכן התמונה המוסתרת, אבל מסתירים את כל הפרטים
         setting_use_pending_items: הסתר עדכוני פיד מאחורי קליק במקום לגלול את הפיד אוטומטית
-        username: שם המשתמש שלך יהיה ייחודי ב- %{domain}
         whole_word: אם מילת מפתח או ביטוי הם אלפאנומריים בלבד, הם יופעלו רק אם נמצאה התאמה למילה שלמה
       domain_allow:
         domain: דומיין זה יוכל לייבא מידע משרת זה והמידע המגיע ממנו יעובד ויאופסן
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
index 81d74cebe..ebc07f0d0 100644
--- a/config/locales/simple_form.hu.yml
+++ b/config/locales/simple_form.hu.yml
@@ -59,7 +59,6 @@ hu:
         setting_show_application: A bejegyzések részletes nézetében látszani fog, milyen alkalmazást használtál a bejegyzés közzétételéhez
         setting_use_blurhash: A kihomályosítás az eredeti képből történik, de minden részletet elrejt
         setting_use_pending_items: Idővonal frissítése csak kattintásra automatikus görgetés helyett
-        username: A felhasználói neved egyedi lesz a %{domain} domainen
         whole_word: Ha a kulcsszó alfanumerikus, csak akkor minősül majd találatnak, ha teljes szóra illeszkedik
       domain_allow:
         domain: Ez a domain adatokat kérhet le erről a kiszolgálóról, és a bejövő adatok fel lesznek dolgozva és tárolva lesznek
diff --git a/config/locales/simple_form.hy.yml b/config/locales/simple_form.hy.yml
index b95502155..2bc72ecdb 100644
--- a/config/locales/simple_form.hy.yml
+++ b/config/locales/simple_form.hy.yml
@@ -49,7 +49,6 @@ hy:
         setting_show_application: Գրառման մանրամասներում կերեւայ թէ որ ծրագրով ես հրապարակել այն
         setting_use_blurhash: Կտորները հիմնուում են թաքցուած վիզուալի վրայ՝ խամրեցնելով դետալները
         setting_use_pending_items: Թաքցնել հոսքի թարմացումները կտտոի ետեւում՝ աւտօմատ թարմացուող հոսքի փոխարէն
-        username: Քո օգտանունը պէտք է եզակի լինի %{domain}-ում։
         whole_word: Եթէ բանալի բառը կամ արտայայտութիւնը պարունակում է միայն այբբենական նիշեր եւ թուեր, ապա այն կիրառուելու է ամբողջ բառի հետ համընկնելու դէպքում միայն
       domain_allow:
         domain: Այս տիրոյթը կարող է ստանալ տուեալներ այս սպասարկչից եւ ստացուող տուեալները կարող են օգտագործուել եւ պահուել
diff --git a/config/locales/simple_form.id.yml b/config/locales/simple_form.id.yml
index 43f278264..2892b1362 100644
--- a/config/locales/simple_form.id.yml
+++ b/config/locales/simple_form.id.yml
@@ -57,7 +57,6 @@ id:
         setting_show_application: Aplikasi yang Anda pakai untuk men-toot akan ditampilkan di tampilan detail toot
         setting_use_blurhash: Gradien didasarkan pada warna visual yang tersembunyi tetapi mengaburkan setiap detail
         setting_use_pending_items: Sembunyikan pembaruan linimasa di balik klik alih-alih bergulir secara otomatis
-        username: Nama pengguna Anda unik di %{domain}
         whole_word: Ketika kata kunci/frasa hanya alfanumerik, maka itu hanya akan diterapkan jika cocok dengan semua kata
       domain_allow:
         domain: Domain ini dapat mengambil data dari server ini dan data yang diterima akan diproses dan disimpan
diff --git a/config/locales/simple_form.io.yml b/config/locales/simple_form.io.yml
index d6aff6afd..d1f4ccf97 100644
--- a/config/locales/simple_form.io.yml
+++ b/config/locales/simple_form.io.yml
@@ -57,7 +57,6 @@ io:
         setting_show_application: Softwaro quon vu uzar por postigar montresos che detala vidajo di vua posti
         setting_use_blurhash: Inklini esas segun kolori di celesis vidaji ma kovras irga detali
         setting_use_pending_items: Celez tempolineonovi dop kliktar e ne automatike movigar niuzeto
-        username: Vua uzantonomo esos nura che %{domain}
         whole_word: Kande klefvorto o fraz esas nur litera e nombra, ol nur aplikesos se ol parigesas la tota vorto
       domain_allow:
         domain: Ca domeno povas ganar informi de ca servilo e venanta informo de ol procedagesos e sparesos
diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml
index ed17c9753..6246099ff 100644
--- a/config/locales/simple_form.is.yml
+++ b/config/locales/simple_form.is.yml
@@ -59,7 +59,6 @@ is:
         setting_show_application: Nafnið á forritinu sem þú notar til að senda færslur mun birtast í ítarlegri sýn á færslunum þínum
         setting_use_blurhash: Litstiglarnir byggja á litunum í földu myndunum, en gera öll smáatriði óskýr
         setting_use_pending_items: Fela uppfærslur tímalínu þar til smellt er, í stað þess að hún skruni streyminu sjálfvirkt
-        username: Notandanafnið þitt verður einstakt á %{domain}
         whole_word: Þegar stikkorð eða setning er einungis tölur og bókstafir, verður það aðeins notað ef það samsvarar heilu orði
       domain_allow:
         domain: Þetta lén mun geta sótt gögn af þessum vefþjóni og tekið verður á móti innsendum gögnum frá léninu til vinnslu og geymslu
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index 189b9bfc5..8e31de783 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -59,7 +59,6 @@ it:
         setting_show_application: L'applicazione che usi per pubblicare i toot sarà mostrata nella vista di dettaglio dei tuoi toot
         setting_use_blurhash: I gradienti sono basati sui colori delle immagini nascoste ma offuscano tutti i dettagli
         setting_use_pending_items: Fare clic per mostrare i nuovi messaggi invece di aggiornare la timeline automaticamente
-        username: Il tuo nome utente sarà unico su %{domain}
         whole_word: Quando la parola chiave o la frase è solo alfanumerica, si applica solo se corrisponde alla parola intera
       domain_allow:
         domain: Questo dominio potrà recuperare i dati da questo server e i dati in arrivo da esso verranno elaborati e memorizzati
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index f7e2cb954..1e52f77c3 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -59,7 +59,6 @@ ja:
         setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
         setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
         setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
-        username: あなたのユーザー名は%{domain}の中で重複していない必要があります
         whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります
       domain_allow:
         domain: 登録するとこのサーバーからデータを受信したり、このドメインから受信するデータを処理して保存できるようになります
diff --git a/config/locales/simple_form.kab.yml b/config/locales/simple_form.kab.yml
index 2f8a9261e..b55a3406d 100644
--- a/config/locales/simple_form.kab.yml
+++ b/config/locales/simple_form.kab.yml
@@ -20,7 +20,6 @@ kab:
         setting_display_media_hide_all: Ffer yal tikkelt akk taywalt
         setting_display_media_show_all: Ffer yal tikkelt teywalt yettwacreḍ d tanafrit
         setting_hide_network: Wid i teṭṭafaṛeḍ d wid i k-yeṭṭafaṛen ur d-ttwaseknen ara deg umaγnu-inek
-        username: Isem-ik n umseqdac ad yili d ayiwen, ulac am netta deg %{domain}
       imports:
         data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun
       ip_block:
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index 1d63fb631..cb1783880 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -59,7 +59,6 @@ ko:
         setting_show_application: 당신이 게시물을 작성하는데에 사용한 앱이 게시물의 상세정보에 표시 됩니다
         setting_use_blurhash: 그라디언트는 숨겨진 내용의 색상을 기반으로 하지만 상세 내용은 보이지 않게 합니다
         setting_use_pending_items: 타임라인의 새 게시물을 자동으로 보여 주는 대신, 클릭해서 나타내도록 합니다
-        username: 당신의 사용자명은 %{domain} 안에서 유일해야 합니다
         whole_word: 키워드가 영문과 숫자로만 이루어 진 경우, 단어 전체에 매칭 되었을 때에만 작동하게 합니다
       domain_allow:
         domain: 이 도메인은 이 서버에서 데이터를 가져갈 수 있고 이 도메인에서 보내진 데이터는 처리되고 저장 됩니다
diff --git a/config/locales/simple_form.ku.yml b/config/locales/simple_form.ku.yml
index f7188152d..bd0529772 100644
--- a/config/locales/simple_form.ku.yml
+++ b/config/locales/simple_form.ku.yml
@@ -59,7 +59,6 @@ ku:
         setting_show_application: Navê sepana ku tu ji bo şandinê wê bi kar tîne dê di dîtinê berferh ên di şandiyên te de were xuyakirin
         setting_use_blurhash: Gradyen xwe bi rengên dîtbarîyên veşartî ve radigire, lê belê hûrgilîyan diveşêre
         setting_use_pending_items: Li şûna ku herkê wek bixweber bizivirînî nûvekirina demnameyê li paş tikandinekî veşêre
-        username: Navê te yê bikarhênerî li ser %{domain} de bêhempa be
         whole_word: Dema peyvkilîd an jî hevok bi tenê alfahejmarî çêbe, bi tenê hemû bêjeyê re li hev bike wê pêk bê
       domain_allow:
         domain: Ev navê navperê, ji vê rajekarê wê daneyan bistîne û daneyên ku jê bê wê were sazkirin û veşartin
diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml
index ff727235e..22ea7c6a9 100644
--- a/config/locales/simple_form.lv.yml
+++ b/config/locales/simple_form.lv.yml
@@ -59,7 +59,6 @@ lv:
         setting_show_application: Lietojumprogramma, ko tu izmanto publicēšanai, tiks parādīta tavu ziņu detalizētajā skatā
         setting_use_blurhash: Gradientu pamatā ir paslēpto vizuālo attēlu krāsas, bet neskaidras visas detaļas
         setting_use_pending_items: Paslēpt laika skalas atjauninājumus aiz klikšķa, nevis automātiski ritini plūsmu
-        username: Tavs lietotājvārds %{domain} būs unikāls
         whole_word: Ja atslēgvārds vai frāze ir tikai burtciparu, tas tiks lietots tikai tad, ja tas atbilst visam vārdam
       domain_allow:
         domain: Šis domēns varēs izgūt datus no šī servera, un no tā ienākošie dati tiks apstrādāti un saglabāti
diff --git a/config/locales/simple_form.my.yml b/config/locales/simple_form.my.yml
index d3594307a..7beaa2a00 100644
--- a/config/locales/simple_form.my.yml
+++ b/config/locales/simple_form.my.yml
@@ -59,7 +59,6 @@ my:
         setting_show_application: ပို့စ်တင်ရန်အတွက် သင်အသုံးပြုသည့် အက်ပလီကေးရှင်းကို သင့်ပို့စ်များ၏ အသေးစိတ်ကြည့်ရှုမှုတွင် ပြသမည်ဖြစ်သည်
         setting_use_blurhash: Gradients မှာ ဖျောက်ထားသောရုပ်ပုံများ၏ အရောင်များကိုအခြေခံသော်လည်း မည်သည့်အသေးစိတ်အချက်အလက်ကိုမဆို ရှုပ်ထွေးစေနိုင်ပါသည်။
         setting_use_pending_items: အပေါ်အောက်လှိမ့်မည့်အစား ကလစ်တစ်ခုနောက်တွင် စာမျက်နှာအပ်ဒိတ်များကို ဖျောက်ထားပါ။
-        username: "%{domain} ရှိ သင့်အသုံးပြုသူအမည်မှာ တူညီ၍မရပါ"
         whole_word: အဓိကစကားလုံး သို့မဟုတ် စကားစုသည် အက္ခရာဂဏန်းများသာဖြစ်ပါကစကားလုံးတစ်ခုလုံးနှင့် ကိုက်ညီမှသာ အသုံးပြုနိုင်မည်ဖြစ်သည်
       domain_allow:
         domain: ဤဒိုမိန်းသည် ဤဆာဗာမှ အချက်အလက်များကို ရယူနိုင်မည်ဖြစ်ပြီး ဝင်လာသောအချက်အလက်များကို စီမံဆောင်ရွက်ပေးပြီး သိမ်းဆည်းသွားမည်ဖြစ်သည်
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 8592857d4..c2b680ec5 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -59,7 +59,6 @@ nl:
         setting_show_application: De toepassing de je gebruikt om berichten te plaatsen wordt in de gedetailleerde weergave van het bericht getoond
         setting_use_blurhash: Wazige kleurovergangen zijn gebaseerd op de kleuren van de verborgen media, waarmee elk detail verdwijnt
         setting_use_pending_items: De tijdlijn wordt bijgewerkt door op het aantal nieuwe items te klikken, in plaats van dat deze automatisch wordt bijgewerkt
-        username: Jouw gebruikersnaam is uniek op %{domain}
         whole_word: Wanneer het trefwoord of zinsdeel alfanumeriek is, wordt het alleen gefilterd wanneer het hele woord overeenkomt
       domain_allow:
         domain: Dit domein is in staat om gegevens van deze server op te halen, en binnenkomende gegevens worden verwerkt en opgeslagen
diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml
index db4007c4f..e3b8fa3fc 100644
--- a/config/locales/simple_form.nn.yml
+++ b/config/locales/simple_form.nn.yml
@@ -57,7 +57,6 @@ nn:
         setting_show_application: Programmet du brukar for å tuta blir vist i den detaljerte visninga av tuta dine
         setting_use_blurhash: Overgangar er basert på fargane til skjulte grafikkelement, men gjer detaljar utydelege
         setting_use_pending_items: Gøym tidslineoppdateringar bak eit klikk, i staden for å rulla ned automatisk
-        username: Brukarnamnet ditt vert unikt på %{domain}
         whole_word: Når søkjeordet eller setninga berre er alfanumerisk, nyttast det berre om det samsvarar med heile ordet
       domain_allow:
         domain: Dette domenet er i stand til å henta data frå denne tenaren og innkomande data vert handsama og lagra
diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml
index 4cf678b83..efd451ce5 100644
--- a/config/locales/simple_form.no.yml
+++ b/config/locales/simple_form.no.yml
@@ -57,7 +57,6 @@
         setting_show_application: Appen du bruker til å publisere innlegg vil bli vist i den detaljerte visningen til innleggene dine
         setting_use_blurhash: Gradientene er basert på fargene til de skjulte visualitetene, men gjør alle detaljer uklare
         setting_use_pending_items: Skjul tidslinjeoppdateringer bak et klikk, i stedet for å automatisk la strømmen skrolle
-        username: Brukernavnet ditt vil være unikt på %{domain}
         whole_word: Når søkeordet eller setningen bare er alfanumerisk, aktiveres det bare hvis det samsvarer med hele ordet
       domain_allow:
         domain: Dette domenet vil være i stand til å hente data fra denne serveren og dets innkommende data vil bli prosessert og lagret
diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml
index 566f4f100..a3ae020b6 100644
--- a/config/locales/simple_form.oc.yml
+++ b/config/locales/simple_form.oc.yml
@@ -52,7 +52,6 @@ oc:
         setting_show_application: Lo nom de l’aplicacion qu’utilizatz per publicar serà mostrat dins la vista detalhada de vòstres tuts
         setting_use_blurhash: Los degradats venon de las colors de l’imatge rescondut en enfoscar los detalhs
         setting_use_pending_items: Rescondre las actualizacions del flux d’actualitat aprèp un clic allòc de desfilar lo flux automaticament
-        username: Vòstre nom d’utilizaire serà unic sus %{domain}
         whole_word: Quand lo mot-clau o frasa es solament alfranumeric, serà pas qu’aplicat se correspond al mot complèt
       domain_allow:
         domain: Aqueste domeni poirà recuperar las donadas d’aqueste servidor estant e las donadas venent d’aqueste domeni seràn tractadas e gardadas
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index 48f1fb745..43f0990ba 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -59,7 +59,6 @@ pl:
         setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany
         setting_use_blurhash: Gradienty są oparte na kolorach ukrywanej zawartości, ale uniewidaczniają wszystkie szczegóły
         setting_use_pending_items: Ukryj aktualizacje osi czasu za kliknięciem, zamiast automatycznego przewijania strumienia
-        username: Twoja nazwa użytkownika będzie niepowtarzalna na %{domain}
         whole_word: Jeśli słowo lub fraza składa się jedynie z liter lub cyfr, filtr będzie zastosowany tylko do pełnych wystąpień
       domain_allow:
         domain: Ta domena będzie mogła pobierać dane z serwera, a dane przychodzące z niej będą przetwarzane i przechowywane
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index bf46305ee..a8fa1fd01 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -59,7 +59,6 @@ pt-BR:
         setting_show_application: O aplicativo que você usar para publicar será exibido na visão detalhada das suas publicações
         setting_use_blurhash: O blur é baseado nas cores da imagem oculta, ofusca a maioria dos detalhes
         setting_use_pending_items: Ocultar atualizações da linha do tempo atrás de um clique ao invés de rolar automaticamente
-        username: Seu nome de usuário será único em %{domain}
         whole_word: Quando a palavra-chave ou frase é inteiramente alfanumérica, ela será aplicada somente se corresponder a palavra inteira
       domain_allow:
         domain: Este domínio poderá obter dados deste servidor e os dados recebidos dele serão processados e armazenados
diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml
index 4e34c5897..de2484d6e 100644
--- a/config/locales/simple_form.pt-PT.yml
+++ b/config/locales/simple_form.pt-PT.yml
@@ -59,7 +59,6 @@ pt-PT:
         setting_show_application: A aplicação que usa para publicar será mostrada na vista pormenorizada das suas publicações
         setting_use_blurhash: Os gradientes são baseados nas cores das imagens escondidas, mas ofuscam quaisquer pormenores
         setting_use_pending_items: Ocultar atualizações da cronologia por detrás dum clique, em vez de rolar automaticamente o fluxo
-        username: O teu nome de utilizador será único em %{domain}
         whole_word: Quando a palavra-chave ou expressão-chave é somente alfanumérica, ela só será aplicada se corresponder à palavra completa
       domain_allow:
         domain: Este domínio será capaz de obter dados desta instância e os dados dele recebidos serão processados e armazenados
diff --git a/config/locales/simple_form.ro.yml b/config/locales/simple_form.ro.yml
index 6ed4905db..af3e000ac 100644
--- a/config/locales/simple_form.ro.yml
+++ b/config/locales/simple_form.ro.yml
@@ -49,7 +49,6 @@ ro:
         setting_show_application: Aplicația pe care o utilizați pentru a posta va fi afișată în vizualizarea detaliată a postărilor
         setting_use_blurhash: Gradienții sunt bazați pe culorile vizualelor ascunse, dar ofuscă orice detalii
         setting_use_pending_items: Ascunde actualizările cronologice din spatele unui click în loc de a derula automat fluxul
-        username: Numele tău de utilizator va fi unic pe %{domain}
         whole_word: Când fraza sau cuvântul este doar alfanumeric, acesta se aplică doar dacă există o potrivire completă
       domain_allow:
         domain: Acest domeniu va putea prelua date de pe acest server și datele primite de la el vor fi procesate și stocate
diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml
index 7e23edf7d..908cd645d 100644
--- a/config/locales/simple_form.ru.yml
+++ b/config/locales/simple_form.ru.yml
@@ -59,7 +59,6 @@ ru:
         setting_show_application: При просмотре поста будет видно из какого приложения он отправлен.
         setting_use_blurhash: Градиенты основаны на цветах скрытых медиа, но скрывают любые детали.
         setting_use_pending_items: Показывать обновления в ленте только после клика вместо автоматической прокрутки.
-        username: Ваше имя пользователя будет уникальным на %{domain}
         whole_word: Если слово или фраза состоит только из букв и цифр, сопоставление произойдёт только по полному совпадению
       domain_allow:
         domain: Этот домен сможет получать данные с этого сервера и его входящие данные будут обрабатываться и сохранены
diff --git a/config/locales/simple_form.sc.yml b/config/locales/simple_form.sc.yml
index 56638352a..07110da1d 100644
--- a/config/locales/simple_form.sc.yml
+++ b/config/locales/simple_form.sc.yml
@@ -53,7 +53,6 @@ sc:
         setting_show_application: S'aplicatzione chi impreas pro publicare tuts at a èssere ammustrada in sa visualizatzione de detàlliu de is tuts
         setting_use_blurhash: Is gradientes sunt basados in subra de is colores de is immàgines cuadas ma imbelant totu is detàllios
         setting_use_pending_items: Cua is atualizatziones in segus de un'incarcu imbetzes de iscùrrere in automàticu su flussu de publicatziones
-        username: Su nòmine de utente tuo at a èssere ùnicu in %{domain}
         whole_word: Cando sa crae (faeddu o fràsia) siat isceti alfanumèrica, s'at a aplicare isceti si currispondet a su faeddu intreu
       domain_allow:
         domain: Custu domìniu at a pòdere recuperare datos dae custu serbidore e is datos in intrada dae cue ant a èssere protzessados e archiviados
diff --git a/config/locales/simple_form.sco.yml b/config/locales/simple_form.sco.yml
index 0dc4fdd79..85f075a15 100644
--- a/config/locales/simple_form.sco.yml
+++ b/config/locales/simple_form.sco.yml
@@ -57,7 +57,6 @@ sco:
         setting_show_application: The application thit ye uise fir tae post wull be displayed in the detailt view o yer posts
         setting_use_blurhash: Gradients is based aff o the colors o the image thit's hid, but ye cannae see onie details
         setting_use_pending_items: Plank timeline updates ahin a chap insteid o automatic scrowin o the feed
-        username: Yer uisernemm wull be a ane aff on %{domain}
         whole_word: Whan the keywird or phrase is alphanumeric ainly, it wull ainly get applied if it matches the haill wird
       domain_allow:
         domain: This domain wull be able tae get data fae this server an data comin in fae it wull get processed an stowed
diff --git a/config/locales/simple_form.si.yml b/config/locales/simple_form.si.yml
index e107d82ac..d62c5a0eb 100644
--- a/config/locales/simple_form.si.yml
+++ b/config/locales/simple_form.si.yml
@@ -57,7 +57,6 @@ si:
         setting_show_application: ඔබ පළ කිරීමට භාවිතා කරන යෙදුම ඔබගේ පළ කිරීම් වල සවිස්තරාත්මක දර්ශනයේ පෙන්වනු ඇත
         setting_use_blurhash: අනුක්‍රමණ සැඟවුණු දෘශ්‍යවල වර්ණ මත පදනම් වන නමුත් ඕනෑම විස්තරයක් අපැහැදිලි කරයි
         setting_use_pending_items: සංග්‍රහය ස්වයංක්‍රීයව අනුචලනය කරනවා වෙනුවට ක්ලික් කිරීමක් පිටුපස කාලරේඛා යාවත්කාලීන සඟවන්න
-        username: ඔබගේ පරිශීලක නාමය %{domain}හි අද්විතීය වනු ඇත
         whole_word: මූල පදය හෝ වාක්‍ය ඛණ්ඩය අක්ෂරාංක පමණක් වන විට, එය යෙදෙන්නේ එය සම්පූර්ණ වචනයට ගැලපේ නම් පමණි
       domain_allow:
         domain: මෙම වසමට මෙම සේවාදායකයෙන් දත්ත ලබා ගැනීමට හැකි වන අතර එයින් ලැබෙන දත්ත සකස් කර ගබඩා කරනු ලැබේ
diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml
index e8fb86f29..35ebca0bb 100644
--- a/config/locales/simple_form.sk.yml
+++ b/config/locales/simple_form.sk.yml
@@ -42,7 +42,6 @@ sk:
         setting_show_application: Aplikácia, ktorú používaš na písanie príspevkov, bude zobrazená v podrobnom náhľade jednotlivých tvojích príspevkov
         setting_use_blurhash: Prechody sú založené na farbách skrytých vizuálov, ale zahaľujú akékoľvek podrobnosti
         setting_use_pending_items: Skry aktualizovanie časovej osi tak, aby bola načitávaná iba po kliknutí, namiesto samostatného posúvania
-        username: Tvoja prezývka bude unikátna pre server %{domain}
         whole_word: Ak je kľúčové slovo, alebo fráza poskladaná iba s písmen a čísel, bude použité iba ak sa zhoduje s celým výrazom
       domain_allow:
         domain: Táto doména bude schopná získavať dáta z tohto servera, a prichádzajúce dáta ním budú spracovávané a uložené
diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml
index ac14735a9..9695ad4de 100644
--- a/config/locales/simple_form.sl.yml
+++ b/config/locales/simple_form.sl.yml
@@ -59,7 +59,6 @@ sl:
         setting_show_application: Aplikacija, ki jo uporabljate za objavljanje, bo prikazana v podrobnem pogledu vaših objav
         setting_use_blurhash: Prelivi temeljijo na barvah skrite vizualne slike, vendar zakrivajo vse podrobnosti
         setting_use_pending_items: Skrij posodobitev časovnice za klikom namesto samodejnega posodabljanja
-        username: Vaše uporabniško ime bo edinstveno na %{domain}
         whole_word: Ko je ključna beseda ali fraza samo alfanumerična, se bo uporabljala le, če se bo ujemala s celotno besedo
       domain_allow:
         domain: Ta domena bo lahko prejela podatke s tega strežnika, dohodni podatki z nje pa bodo obdelani in shranjeni
diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml
index a11037fa9..ddbebdf57 100644
--- a/config/locales/simple_form.sq.yml
+++ b/config/locales/simple_form.sq.yml
@@ -59,7 +59,6 @@ sq:
         setting_show_application: Aplikacioni që përdorni për mesazhe do të shfaqet te pamja e hollësishme për mesazhet tuaj
         setting_use_blurhash: Gradientët bazohen në ngjyrat e elementëve pamorë të fshehur, por errësojnë çfarëdo hollësie
         setting_use_pending_items: Fshihi përditësimet e rrjedhës kohore pas një klikimi, në vend të rrëshqitjes automatike nëpër prurje
-        username: Emri juaj i përdoruesit do të jetë unik në %{domain}
         whole_word: Kur fjalëkyçi ose fraza është vetëm numerike, do të aplikohet vetëm nëse përputhet me krejt fjalën
       domain_allow:
         domain: Kjo përkatësi do të jetë në gjendje të sjellë të dhëna prej këtij shërbyesi dhe të dhënat ardhëse prej tij do të përpunohen dhe depozitohen
diff --git a/config/locales/simple_form.sr-Latn.yml b/config/locales/simple_form.sr-Latn.yml
index e987145c1..e268fd20e 100644
--- a/config/locales/simple_form.sr-Latn.yml
+++ b/config/locales/simple_form.sr-Latn.yml
@@ -59,7 +59,6 @@ sr-Latn:
         setting_show_application: Aplikacija koju koristite za objavljivanje će biti prikazana u detaljnom prikazu vaših objava
         setting_use_blurhash: Gradijenti se formiraju na osnovu bojâ skrivenih slika i zamućuju prikaz, prikrivajući detalje
         setting_use_pending_items: Sakriva ažuriranja vremenske linije iza klika umesto automatskog ažuriranja i pomeranja vremenske linije
-        username: Vaš nadimak će biti jedinstven na %{domain}
         whole_word: Kada je ključna reč ili fraza isključivo alfanumerička, biće primenjena samo ako se podudara sa celom rečju
       domain_allow:
         domain: Ovaj domen će moći da preuzima podatke sa ovog servera i dolazni podaci sa njega će se obrađivati i čuvati
diff --git a/config/locales/simple_form.sr.yml b/config/locales/simple_form.sr.yml
index 8ee586377..9c412cd60 100644
--- a/config/locales/simple_form.sr.yml
+++ b/config/locales/simple_form.sr.yml
@@ -59,7 +59,6 @@ sr:
         setting_show_application: Апликација коју користите за објављивање ће бити приказана у детаљном приказу ваших објава
         setting_use_blurhash: Градијенти се формирају на основу бојâ скривених слика и замућују приказ, прикривајући детаље
         setting_use_pending_items: Сакрива ажурирања временске линије иза клика уместо аутоматског ажурирања и померања временске линије
-        username: Ваш надимак ће бити јединствен на %{domain}
         whole_word: Када је кључна реч или фраза искључиво алфанумеричка, биће примењена само ако се подудара са целом речjу
       domain_allow:
         domain: Овај домен ће моћи да преузима податке са овог сервера и долазни подаци са њега ће се обрађивати и чувати
diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml
index cf914b058..60847fc94 100644
--- a/config/locales/simple_form.sv.yml
+++ b/config/locales/simple_form.sv.yml
@@ -59,7 +59,6 @@ sv:
         setting_show_application: Applikationen du använder för att göra inlägg kommer visas i detaljvyn för dina inlägg
         setting_use_blurhash: Gradienter är baserade på färgerna av de dolda objekten men fördunklar alla detaljer
         setting_use_pending_items: Dölj tidslinjeuppdateringar bakom ett klick istället för att automatiskt bläddra i flödet
-        username: Ditt användarnamn måste vara unikt på %{domain}
         whole_word: När sökordet eller frasen endast är alfanumerisk, kommer det endast att tillämpas om det matchar hela ordet
       domain_allow:
         domain: Denna domän kommer att kunna hämta data från denna server och inkommande data från den kommer att behandlas och lagras
diff --git a/config/locales/simple_form.th.yml b/config/locales/simple_form.th.yml
index 345f8c183..8fddeaf42 100644
--- a/config/locales/simple_form.th.yml
+++ b/config/locales/simple_form.th.yml
@@ -59,7 +59,6 @@ th:
         setting_show_application: จะแสดงแอปพลิเคชันที่คุณใช้ในการโพสต์ในมุมมองโดยละเอียดของโพสต์ของคุณ
         setting_use_blurhash: การไล่ระดับสีอิงตามสีของภาพที่ซ่อนอยู่แต่ทำให้รายละเอียดใด ๆ คลุมเครือ
         setting_use_pending_items: ซ่อนการอัปเดตเส้นเวลาไว้หลังการคลิกแทนที่จะเลื่อนฟีดโดยอัตโนมัติ
-        username: ชื่อผู้ใช้ของคุณจะไม่ซ้ำกันใน %{domain}
         whole_word: เมื่อคำสำคัญหรือวลีเป็นตัวอักษรและตัวเลขเท่านั้น จะนำไปใช้กับคำสำคัญหรือวลีหากตรงกันทั้งคำเท่านั้น
       domain_allow:
         domain: โดเมนนี้จะสามารถดึงข้อมูลจากเซิร์ฟเวอร์นี้และจะประมวลผลและจัดเก็บข้อมูลขาเข้าจากโดเมน
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index a2f42e259..586731466 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -59,7 +59,6 @@ tr:
         setting_show_application: Gönderi gönderimi için kullandığınız uygulama, gönderilerinizin ayrıntılı görünümünde gösterilecektir
         setting_use_blurhash: Gradyenler gizli görsellerin renklerine dayanır, ancak detayları gizler
         setting_use_pending_items: Akışı otomatik olarak kaydırmak yerine, zaman çizelgesi güncellemelerini tek bir tıklamayla gizleyin
-        username: Kullanıcı adınız %{domain} alanında benzersiz olacak
         whole_word: Anahtar kelime veya kelime öbeği yalnızca alfasayısal olduğunda, yalnızca tüm sözcükle eşleşirse uygulanır
       domain_allow:
         domain: Bu alan adı, bu sunucudan veri alabilecek ve ondan gelen veri işlenecek ve saklanacaktır
diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml
index be4d4b764..fa67454d6 100644
--- a/config/locales/simple_form.uk.yml
+++ b/config/locales/simple_form.uk.yml
@@ -59,7 +59,6 @@ uk:
         setting_show_application: Застосунок, за допомогою якого ви зробили допис, буде показано серед подробиць допису
         setting_use_blurhash: Градієнти, що базуються на кольорах прихованих медіа, але роблять нерозрізненними будь-які деталі
         setting_use_pending_items: Не додавати нові повідомлення до стрічок миттєво, показувати лише після додаткового клацання
-        username: Ваше ім'я користувача буде унікальним у %{domain}
         whole_word: Якщо пошукове слово або фраза містить лише літери та цифри, воно має збігатися цілком
       domain_allow:
         domain: Цей домен зможе отримувати дані з цього сервера. Вхідні дані будуть оброблені та збережені
diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml
index b5da24dd1..d0e7d165b 100644
--- a/config/locales/simple_form.vi.yml
+++ b/config/locales/simple_form.vi.yml
@@ -59,7 +59,6 @@ vi:
         setting_show_application: Tên ứng dụng bạn dùng để đăng tút sẽ hiện trong chi tiết của tút
         setting_use_blurhash: Lớp phủ mờ dựa trên màu sắc của hình ảnh nhạy cảm
         setting_use_pending_items: Dồn lại toàn bộ tút mới và chỉ hiển thị khi nhấn vào
-        username: Tên người dùng của bạn sẽ là duy nhất trên %{domain}
         whole_word: Khi từ khóa hoặc cụm từ là chữ và số, nó sẽ chỉ hiện ra những từ chính xác như vậy
       domain_allow:
         domain: Máy chủ này sẽ tiếp nhận dữ liệu, rồi sau đó xử lý và lưu trữ
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index 8de23085d..2dc84f11a 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -59,7 +59,6 @@ zh-CN:
         setting_show_application: 你用来发表嘟文的应用程序将会在你嘟文的详细内容中显示
         setting_use_blurhash: 渐变是基于模糊后的隐藏内容生成的
         setting_use_pending_items: 关闭自动滚动更新,时间轴会在点击后更新
-        username: 你的用户名在 %{domain} 上是唯一的
         whole_word: 如果关键词只包含字母和数字,将只在词语完全匹配时才会应用
       domain_allow:
         domain: 该站点将能够从该服务器上拉取数据,并处理和存储收到的数据。
diff --git a/config/locales/simple_form.zh-HK.yml b/config/locales/simple_form.zh-HK.yml
index 02ef592c9..d1ccbbe8a 100644
--- a/config/locales/simple_form.zh-HK.yml
+++ b/config/locales/simple_form.zh-HK.yml
@@ -59,7 +59,6 @@ zh-HK:
         setting_show_application: 你用來發表文章的應用程式,將會顯示在你文章的詳細檢視中
         setting_use_blurhash: 漸變圖樣會基於隱藏媒體內容產生,但所有細節會變得模糊
         setting_use_pending_items: 關閉自動滾動更新,時間軸會在點擊後更新
-        username: 你的使用者名稱在 %{domain} 將是獨一無二的
         whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用
       domain_allow:
         domain: 此網域將能從此站獲取資料,而此站發出的數據也會被處理和存儲。
diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml
index 882f1d3ff..d2c1213f2 100644
--- a/config/locales/simple_form.zh-TW.yml
+++ b/config/locales/simple_form.zh-TW.yml
@@ -59,7 +59,6 @@ zh-TW:
         setting_show_application: 您用來發嘟文的應用程式將會在您嘟文的詳細檢視顯示
         setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節會變得模糊
         setting_use_pending_items: 關閉自動捲動更新,時間軸只會在點擊後更新
-        username: 您的使用者名稱將於 %{domain} 是獨一無二的
         whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用
       domain_allow:
         domain: 此網域將能夠攫取本站資料,而自該網域發出的資料也會於本站處理和留存。
diff --git a/config/routes.rb b/config/routes.rb
index 099933116..26d5c3526 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -667,9 +667,33 @@ Rails.application.routes.draw do
         resources :ip_blocks, only: [:index, :show, :update, :create, :destroy]
 
         namespace :trends do
-          resources :tags, only: [:index]
-          resources :links, only: [:index]
-          resources :statuses, only: [:index]
+          resources :tags, only: [:index] do
+            member do
+              post :approve
+              post :reject
+            end
+          end
+          resources :links, only: [:index] do
+            member do
+              post :approve
+              post :reject
+            end
+          end
+          resources :statuses, only: [:index] do
+            member do
+              post :approve
+              post :reject
+            end
+          end
+
+          namespace :links do
+            resources :preview_card_providers, only: [:index], path: :publishers do
+              member do
+                post :approve
+                post :reject
+              end
+            end
+          end
         end
 
         post :measures, to: 'measures#create'
diff --git a/jest.config.js b/jest.config.js
index 1eb143a59..d6e7bc65f 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -13,7 +13,7 @@ const config = {
   setupFiles: ['raf/polyfill'],
   setupFilesAfterEnv: ['<rootDir>/app/javascript/mastodon/test_setup.js'],
   collectCoverageFrom: [
-    'app/javascript/mastodon/**/*.js',
+    'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}',
     '!app/javascript/mastodon/features/emoji/emoji_compressed.js',
     '!app/javascript/mastodon/locales/locale-data/*.js',
     '!app/javascript/mastodon/service_worker/entry.js',
diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb
index a6532541e..5194cd80a 100644
--- a/lib/mastodon/accounts_cli.rb
+++ b/lib/mastodon/accounts_cli.rb
@@ -57,6 +57,7 @@ module Mastodon
     option :role
     option :reattach, type: :boolean
     option :force, type: :boolean
+    option :approve, type: :boolean
     desc 'create USERNAME', 'Create a new user account'
     long_desc <<-LONG_DESC
       Create a new user account with a given USERNAME and an
@@ -72,6 +73,8 @@ module Mastodon
       account is still in use by someone else, you can supply
       the --force option to delete the old record and reattach the
       username to the new account anyway.
+
+      With the --approve option, the account will be approved.
     LONG_DESC
     def create(username)
       role_id  = nil
@@ -89,7 +92,7 @@ module Mastodon
 
       account  = Account.new(username: username)
       password = SecureRandom.hex
-      user     = User.new(email: options[:email], password: password, agreement: true, approved: true, role_id: role_id, confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
+      user     = User.new(email: options[:email], password: password, agreement: true, role_id: role_id, confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
 
       if options[:reattach]
         account = Account.find_local(username) || Account.new(username: username)
@@ -112,6 +115,8 @@ module Mastodon
           user.confirm!
         end
 
+        user.approve! if options[:approve]
+
         say('OK', :green)
         say("New password: #{password}")
       else
@@ -184,9 +189,10 @@ module Mastodon
       user.disabled = true if options[:disable]
       user.approved = true if options[:approve]
       user.otp_required_for_login = false if options[:disable_2fa]
-      user.confirm if options[:confirm]
 
       if user.save
+        user.confirm if options[:confirm]
+
         say('OK', :green)
         say("New password: #{password}") if options[:reset_password]
       else
diff --git a/package.json b/package.json
index 051feb816..6d6025d50 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,7 @@
     "fuzzysort": "^2.0.4",
     "glob": "^10.0.0",
     "history": "^4.10.1",
-    "http-link-header": "^1.1.0",
+    "http-link-header": "^1.1.1",
     "immutable": "^4.3.0",
     "imports-loader": "^1.2.0",
     "intl": "^1.2.5",
@@ -86,7 +86,7 @@
     "path-complete-extname": "^1.0.0",
     "pg": "^8.5.0",
     "pg-connection-string": "^2.5.0",
-    "postcss": "^8.4.21",
+    "postcss": "^8.4.22",
     "postcss-loader": "^4.3.0",
     "promise.prototype.finally": "^3.1.4",
     "prop-types": "^15.8.1",
@@ -116,7 +116,7 @@
     "redux-thunk": "^2.4.2",
     "regenerator-runtime": "^0.13.11",
     "requestidlecallback": "^0.3.0",
-    "reselect": "^4.1.7",
+    "reselect": "^4.1.8",
     "rimraf": "^5.0.0",
     "sass": "^1.61.0",
     "sass-loader": "^10.2.0",
diff --git a/spec/controllers/api/v1/admin/trends/links/preview_card_providers_controller_spec.rb b/spec/controllers/api/v1/admin/trends/links/preview_card_providers_controller_spec.rb
new file mode 100644
index 000000000..883a55b7b
--- /dev/null
+++ b/spec/controllers/api/v1/admin/trends/links/preview_card_providers_controller_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::Trends::Links::PreviewCardProvidersController do
+  render_views
+
+  let(:role)   { UserRole.find_by(name: 'Admin') }
+  let(:user)   { Fabricate(:user, role: role) }
+  let(:scopes) { 'admin:read admin:write' }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:account) { Fabricate(:account) }
+  let(:preview_card_provider) { Fabricate(:preview_card_provider) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  shared_examples 'forbidden for wrong role' do |wrong_role|
+    let(:role) { UserRole.find_by(name: wrong_role) }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST #approve' do
+    before do
+      post :approve, params: { id: preview_card_provider.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST #reject' do
+    before do
+      post :reject, params: { id: preview_card_provider.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/trends/links_controller_spec.rb b/spec/controllers/api/v1/admin/trends/links_controller_spec.rb
index a64292f06..9c144d3fa 100644
--- a/spec/controllers/api/v1/admin/trends/links_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/trends/links_controller_spec.rb
@@ -5,14 +5,33 @@ require 'rails_helper'
 describe Api::V1::Admin::Trends::LinksController do
   render_views
 
-  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:role)   { UserRole.find_by(name: 'Admin') }
+  let(:user)   { Fabricate(:user, role: role) }
+  let(:scopes) { 'admin:read admin:write' }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:account) { Fabricate(:account) }
+  let(:preview_card) { Fabricate(:preview_card) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  shared_examples 'forbidden for wrong role' do |wrong_role|
+    let(:role) { UserRole.find_by(name: wrong_role) }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
   describe 'GET #index' do
     it 'returns http success' do
       get :index, params: { account_id: account.id, limit: 2 }
@@ -20,4 +39,30 @@ describe Api::V1::Admin::Trends::LinksController do
       expect(response).to have_http_status(200)
     end
   end
+
+  describe 'POST #approve' do
+    before do
+      post :approve, params: { id: preview_card.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST #reject' do
+    before do
+      post :reject, params: { id: preview_card.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
 end
diff --git a/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb b/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb
index 821cc499f..d25186b37 100644
--- a/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb
@@ -5,14 +5,33 @@ require 'rails_helper'
 describe Api::V1::Admin::Trends::StatusesController do
   render_views
 
-  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:role)   { UserRole.find_by(name: 'Admin') }
+  let(:user)   { Fabricate(:user, role: role) }
+  let(:scopes) { 'admin:read admin:write' }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:account) { Fabricate(:account) }
+  let(:status)  { Fabricate(:status) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  shared_examples 'forbidden for wrong role' do |wrong_role|
+    let(:role) { UserRole.find_by(name: wrong_role) }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
   describe 'GET #index' do
     it 'returns http success' do
       get :index, params: { account_id: account.id, limit: 2 }
@@ -20,4 +39,30 @@ describe Api::V1::Admin::Trends::StatusesController do
       expect(response).to have_http_status(200)
     end
   end
+
+  describe 'POST #approve' do
+    before do
+      post :approve, params: { id: status.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST #reject' do
+    before do
+      post :reject, params: { id: status.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
 end
diff --git a/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb b/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb
index 480306ce7..5ee443d57 100644
--- a/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb
@@ -5,14 +5,33 @@ require 'rails_helper'
 describe Api::V1::Admin::Trends::TagsController do
   render_views
 
-  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
-  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:role)   { UserRole.find_by(name: 'Admin') }
+  let(:user)   { Fabricate(:user, role: role) }
+  let(:scopes) { 'admin:read admin:write' }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:account) { Fabricate(:account) }
+  let(:tag)     { Fabricate(:tag) }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
+  shared_examples 'forbidden for wrong scope' do |wrong_scope|
+    let(:scopes) { wrong_scope }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
+  shared_examples 'forbidden for wrong role' do |wrong_role|
+    let(:role) { UserRole.find_by(name: wrong_role) }
+
+    it 'returns http forbidden' do
+      expect(response).to have_http_status(403)
+    end
+  end
+
   describe 'GET #index' do
     it 'returns http success' do
       get :index, params: { account_id: account.id, limit: 2 }
@@ -20,4 +39,30 @@ describe Api::V1::Admin::Trends::TagsController do
       expect(response).to have_http_status(200)
     end
   end
+
+  describe 'POST #approve' do
+    before do
+      post :approve, params: { id: tag.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
+
+  describe 'POST #reject' do
+    before do
+      post :reject, params: { id: tag.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', ''
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+  end
 end
diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb
index a6db08d85..341fe6f23 100644
--- a/spec/mailers/notification_mailer_spec.rb
+++ b/spec/mailers/notification_mailer_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe NotificationMailer, type: :mailer do
 
     it 'renders the headers' do
       expect(mail.subject).to eq('You were mentioned by bob')
-      expect(mail.to).to eq([receiver.email])
+      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
     end
 
     it 'renders the body' do
@@ -46,7 +46,7 @@ RSpec.describe NotificationMailer, type: :mailer do
 
     it 'renders the headers' do
       expect(mail.subject).to eq('bob is now following you')
-      expect(mail.to).to eq([receiver.email])
+      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
     end
 
     it 'renders the body' do
@@ -62,7 +62,7 @@ RSpec.describe NotificationMailer, type: :mailer do
 
     it 'renders the headers' do
       expect(mail.subject).to eq('bob favourited your post')
-      expect(mail.to).to eq([receiver.email])
+      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
     end
 
     it 'renders the body' do
@@ -79,7 +79,7 @@ RSpec.describe NotificationMailer, type: :mailer do
 
     it 'renders the headers' do
       expect(mail.subject).to eq('bob boosted your post')
-      expect(mail.to).to eq([receiver.email])
+      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
     end
 
     it 'renders the body' do
@@ -96,7 +96,7 @@ RSpec.describe NotificationMailer, type: :mailer do
 
     it 'renders the headers' do
       expect(mail.subject).to eq('Pending follower: bob')
-      expect(mail.to).to eq([receiver.email])
+      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
     end
 
     it 'renders the body' do
diff --git a/spec/models/account_filter_spec.rb b/spec/models/account_filter_spec.rb
index 3032260fe..cb00e7609 100644
--- a/spec/models/account_filter_spec.rb
+++ b/spec/models/account_filter_spec.rb
@@ -44,4 +44,23 @@ describe AccountFilter do
       expect(filter.results).to match_array [remote_account_one]
     end
   end
+
+  describe 'with username' do
+    let!(:local_account) { Fabricate(:account, domain: nil, username: 'validUserName') }
+
+    it 'works with @ at the beginning of the username' do
+      filter = described_class.new(username: '@validUserName')
+      expect(filter.results).to match_array [local_account]
+    end
+
+    it 'does not work with more than one @ at the beginning of the username' do
+      filter = described_class.new(username: '@@validUserName')
+      expect(filter.results).to_not match_array [local_account]
+    end
+
+    it 'does not work with @ outside the beginning of the username' do
+      filter = described_class.new(username: 'validUserName@')
+      expect(filter.results).to_not match_array [local_account]
+    end
+  end
 end
diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb
index fdf5ec923..2ad6d30f6 100644
--- a/spec/services/reblog_service_spec.rb
+++ b/spec/services/reblog_service_spec.rb
@@ -35,13 +35,25 @@ RSpec.describe ReblogService, type: :service do
   end
 
   context 'when the reblogged status is discarded in the meantime' do
-    let(:status) { Fabricate(:status, account: alice, visibility: :public) }
+    let(:status) { Fabricate(:status, account: alice, visibility: :public, text: 'discard-status-text') }
 
+    # Add a callback to discard the status being reblogged after the
+    # validations pass but before the database commit is executed.
     before do
-      # Update the in-database attribute without reflecting the change in
-      # the object. This cannot simulate all race conditions, but it is
-      # pretty close.
-      Status.where(id: status.id).update_all(deleted_at: Time.now.utc) # rubocop:disable Rails/SkipsModelValidations
+      Status.class_eval do
+        before_save :discard_status
+        def discard_status
+          Status
+            .where(id: reblog_of_id)
+            .where(text: 'discard-status-text')
+            .update_all(deleted_at: Time.now.utc) # rubocop:disable Rails/SkipsModelValidations
+        end
+      end
+    end
+
+    # Remove race condition simulating `discard_status` callback.
+    after do
+      Status._save_callbacks.delete(:discard_status)
     end
 
     it 'raises an exception' do
diff --git a/yarn.lock b/yarn.lock
index fbd8b5255..f36c6b2c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6047,10 +6047,10 @@ http-errors@~1.6.2:
     setprototypeof "1.1.0"
     statuses ">= 1.4.0 < 2"
 
-http-link-header@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-1.1.0.tgz#a1ca87efdbcb7778d8d0d4525de1e6964ec1f129"
-  integrity sha512-pj6N1yxOz/ANO8HHsWGg/OoIL1kmRYvQnXQ7PIRpgp+15AnEsRH8fmIJE6D1OdWG2Bov+BJHVla1fFXxg1JbbA==
+http-link-header@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-1.1.1.tgz#f0e6971b0ed86e858d2077066ecb7ba4f2e50de9"
+  integrity sha512-mW3N/rTYpCn99s1do0zx6nzFZSwLH9HGfUM4ZqLWJ16ylmYaC2v5eYGqrNTQlByx8AzUgGI+V/32gXPugs1+Sw==
 
 http-parser-js@>=0.5.1:
   version "0.5.3"
@@ -8020,10 +8020,10 @@ nan@^2.12.1:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
   integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
 
-nanoid@^3.3.4:
-  version "3.3.4"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
-  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+nanoid@^3.3.6:
+  version "3.3.6"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+  integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -9022,12 +9022,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
 
-postcss@^8.2.15, postcss@^8.4.21:
-  version "8.4.21"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
-  integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
+postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.22:
+  version "8.4.22"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.22.tgz#c29e6776b60ab3af602d4b513d5bd2ff9aa85dc1"
+  integrity sha512-XseknLAfRHzVWjCEtdviapiBtfLdgyzExD50Rg2ePaucEesyh8Wv4VPdW0nbyDa1ydbrAxV19jvMT4+LFmcNUA==
   dependencies:
-    nanoid "^3.3.4"
+    nanoid "^3.3.6"
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
@@ -9804,10 +9804,10 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
-reselect@^4.1.7:
-  version "4.1.7"
-  resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42"
-  integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==
+reselect@^4.1.8:
+  version "4.1.8"
+  resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524"
+  integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
 
 resolve-cwd@^2.0.0:
   version "2.0.0"