about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2019-09-30 12:23:57 +0200
committerThibaut Girka <thib@sitedethib.com>2019-09-30 12:23:57 +0200
commit16ff7c5627c12a0c9658e9d2fac7c48002e1b788 (patch)
tree465a73fb9f42bc2b01127b2d477b0715fb6185b4 /app
parentfebcdad2e2c98aee62b55ee21bdf0debf7c6fd6b (diff)
parent3babf8464b0903b854ec16d355909444ef3ca0bc (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- Gemfile
- Gemfile.lock
- app/controllers/about_controller.rb
- app/controllers/auth/sessions_controller.rb
Diffstat (limited to 'app')
-rw-r--r--app/controllers/about_controller.rb43
-rw-r--r--app/controllers/accounts_controller.rb1
-rw-r--r--app/controllers/activitypub/collections_controller.rb6
-rw-r--r--app/controllers/admin/relays_controller.rb7
-rw-r--r--app/controllers/admin/two_factor_authentications_controller.rb1
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts_controller.rb2
-rw-r--r--app/controllers/api/v2/search_controller.rb2
-rw-r--r--app/controllers/auth/challenges_controller.rb22
-rw-r--r--app/controllers/auth/sessions_controller.rb61
-rw-r--r--app/controllers/concerns/challengable_concern.rb65
-rw-r--r--app/controllers/concerns/export_controller_concern.rb7
-rw-r--r--app/controllers/custom_css_controller.rb1
-rw-r--r--app/controllers/directories_controller.rb2
-rw-r--r--app/controllers/follower_accounts_controller.rb1
-rw-r--r--app/controllers/following_accounts_controller.rb1
-rw-r--r--app/controllers/manifests_controller.rb1
-rw-r--r--app/controllers/media_controller.rb1
-rw-r--r--app/controllers/media_proxy_controller.rb1
-rw-r--r--app/controllers/remote_follow_controller.rb2
-rw-r--r--app/controllers/remote_interaction_controller.rb2
-rw-r--r--app/controllers/settings/aliases_controller.rb43
-rw-r--r--app/controllers/settings/exports_controller.rb7
-rw-r--r--app/controllers/settings/migration/redirects_controller.rb45
-rw-r--r--app/controllers/settings/migrations_controller.rb39
-rw-r--r--app/controllers/settings/two_factor_authentication/confirmations_controller.rb5
-rw-r--r--app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb6
-rw-r--r--app/controllers/settings/two_factor_authentications_controller.rb4
-rw-r--r--app/controllers/statuses_controller.rb1
-rw-r--r--app/controllers/tags_controller.rb2
-rw-r--r--app/controllers/well_known/nodeinfo_controller.rb19
-rw-r--r--app/helpers/settings_helper.rb8
-rw-r--r--app/javascript/mastodon/actions/blocks.js14
-rw-r--r--app/javascript/mastodon/actions/compose.js6
-rw-r--r--app/javascript/mastodon/actions/conversations.js28
-rw-r--r--app/javascript/mastodon/actions/importer/normalizer.js3
-rw-r--r--app/javascript/mastodon/actions/notifications.js11
-rw-r--r--app/javascript/mastodon/components/avatar_composite.js28
-rw-r--r--app/javascript/mastodon/components/column_header.js4
-rw-r--r--app/javascript/mastodon/components/poll.js27
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js1
-rw-r--r--app/javascript/mastodon/containers/media_container.js16
-rw-r--r--app/javascript/mastodon/containers/status_container.js19
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js15
-rw-r--r--app/javascript/mastodon/features/direct_timeline/components/conversation.js128
-rw-r--r--app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js75
-rw-r--r--app/javascript/mastodon/features/emoji/emoji.js2
-rw-r--r--app/javascript/mastodon/features/favourites/index.js4
-rw-r--r--app/javascript/mastodon/features/followers/index.js6
-rw-r--r--app/javascript/mastodon/features/following/index.js6
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js8
-rw-r--r--app/javascript/mastodon/features/notifications/components/filter_bar.js2
-rw-r--r--app/javascript/mastodon/features/notifications/index.js7
-rw-r--r--app/javascript/mastodon/features/reblogs/index.js4
-rw-r--r--app/javascript/mastodon/features/status/containers/detailed_status_container.js18
-rw-r--r--app/javascript/mastodon/features/status/index.js20
-rw-r--r--app/javascript/mastodon/features/ui/components/block_modal.js103
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.js12
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js27
-rw-r--r--app/javascript/mastodon/features/ui/components/mute_modal.js15
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js4
-rw-r--r--app/javascript/mastodon/locales/ar.json33
-rw-r--r--app/javascript/mastodon/locales/ast.json5
-rw-r--r--app/javascript/mastodon/locales/bg.json19
-rw-r--r--app/javascript/mastodon/locales/bn.json5
-rw-r--r--app/javascript/mastodon/locales/br.json95
-rw-r--r--app/javascript/mastodon/locales/ca.json5
-rw-r--r--app/javascript/mastodon/locales/co.json11
-rw-r--r--app/javascript/mastodon/locales/cs.json9
-rw-r--r--app/javascript/mastodon/locales/cy.json61
-rw-r--r--app/javascript/mastodon/locales/da.json5
-rw-r--r--app/javascript/mastodon/locales/de.json11
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json55
-rw-r--r--app/javascript/mastodon/locales/el.json17
-rw-r--r--app/javascript/mastodon/locales/en.json5
-rw-r--r--app/javascript/mastodon/locales/eo.json65
-rw-r--r--app/javascript/mastodon/locales/es-AR.json419
-rw-r--r--app/javascript/mastodon/locales/es.json15
-rw-r--r--app/javascript/mastodon/locales/et.json5
-rw-r--r--app/javascript/mastodon/locales/eu.json5
-rw-r--r--app/javascript/mastodon/locales/fa.json37
-rw-r--r--app/javascript/mastodon/locales/fi.json5
-rw-r--r--app/javascript/mastodon/locales/fr.json33
-rw-r--r--app/javascript/mastodon/locales/ga.json420
-rw-r--r--app/javascript/mastodon/locales/gl.json9
-rw-r--r--app/javascript/mastodon/locales/he.json5
-rw-r--r--app/javascript/mastodon/locales/hi.json5
-rw-r--r--app/javascript/mastodon/locales/hr.json5
-rw-r--r--app/javascript/mastodon/locales/hu.json57
-rw-r--r--app/javascript/mastodon/locales/hy.json5
-rw-r--r--app/javascript/mastodon/locales/id.json5
-rw-r--r--app/javascript/mastodon/locales/io.json5
-rw-r--r--app/javascript/mastodon/locales/it.json29
-rw-r--r--app/javascript/mastodon/locales/ja.json21
-rw-r--r--app/javascript/mastodon/locales/ka.json5
-rw-r--r--app/javascript/mastodon/locales/kk.json5
-rw-r--r--app/javascript/mastodon/locales/ko.json11
-rw-r--r--app/javascript/mastodon/locales/lt.json5
-rw-r--r--app/javascript/mastodon/locales/lv.json5
-rw-r--r--app/javascript/mastodon/locales/ms.json5
-rw-r--r--app/javascript/mastodon/locales/nl.json33
-rw-r--r--app/javascript/mastodon/locales/nn.json15
-rw-r--r--app/javascript/mastodon/locales/no.json5
-rw-r--r--app/javascript/mastodon/locales/oc.json57
-rw-r--r--app/javascript/mastodon/locales/pl.json11
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json51
-rw-r--r--app/javascript/mastodon/locales/pt-PT.json5
-rw-r--r--app/javascript/mastodon/locales/ro.json5
-rw-r--r--app/javascript/mastodon/locales/ru.json5
-rw-r--r--app/javascript/mastodon/locales/sk.json11
-rw-r--r--app/javascript/mastodon/locales/sl.json5
-rw-r--r--app/javascript/mastodon/locales/sq.json5
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json5
-rw-r--r--app/javascript/mastodon/locales/sr.json5
-rw-r--r--app/javascript/mastodon/locales/sv.json5
-rw-r--r--app/javascript/mastodon/locales/ta.json5
-rw-r--r--app/javascript/mastodon/locales/te.json5
-rw-r--r--app/javascript/mastodon/locales/th.json13
-rw-r--r--app/javascript/mastodon/locales/tr.json5
-rw-r--r--app/javascript/mastodon/locales/uk.json5
-rw-r--r--app/javascript/mastodon/locales/whitelist_es-AR.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_ga.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json5
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json5
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json5
-rw-r--r--app/javascript/mastodon/reducers/blocks.js22
-rw-r--r--app/javascript/mastodon/reducers/compose.js7
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/mutes.js2
-rw-r--r--app/javascript/mastodon/reducers/notifications.js11
-rw-r--r--app/javascript/mastodon/reducers/timelines.js5
-rw-r--r--app/javascript/mastodon/utils/scrollbar.js36
-rw-r--r--app/javascript/packs/public.js10
-rw-r--r--app/javascript/styles/mastodon-light/diff.scss2
-rw-r--r--app/javascript/styles/mastodon/_mixins.scss18
-rw-r--r--app/javascript/styles/mastodon/about.scss179
-rw-r--r--app/javascript/styles/mastodon/admin.scss181
-rw-r--r--app/javascript/styles/mastodon/basics.scss3
-rw-r--r--app/javascript/styles/mastodon/components.scss219
-rw-r--r--app/javascript/styles/mastodon/containers.scss62
-rw-r--r--app/javascript/styles/mastodon/forms.scss8
-rw-r--r--app/javascript/styles/mastodon/polls.scss10
-rw-r--r--app/javascript/styles/mastodon/tables.scss67
-rw-r--r--app/javascript/styles/mastodon/widgets.scss83
-rw-r--r--app/lib/activity_tracker.rb2
-rw-r--r--app/lib/activitypub/activity/create.rb40
-rw-r--r--app/lib/activitypub/activity/follow.rb2
-rw-r--r--app/lib/activitypub/activity/move.rb6
-rw-r--r--app/lib/activitypub/adapter.rb1
-rw-r--r--app/lib/nodeinfo/adapter.rb7
-rw-r--r--app/lib/toc_generator.rb69
-rw-r--r--app/mailers/user_mailer.rb33
-rw-r--r--app/models/account_alias.rb47
-rw-r--r--app/models/account_migration.rb78
-rw-r--r--app/models/concerns/account_associations.rb2
-rw-r--r--app/models/concerns/ldap_authenticable.rb44
-rw-r--r--app/models/domain_block.rb1
-rw-r--r--app/models/form/challenge.rb8
-rw-r--r--app/models/form/migration.rb25
-rw-r--r--app/models/form/redirect.rb47
-rw-r--r--app/models/poll.rb5
-rw-r--r--app/models/preview_card.rb4
-rw-r--r--app/models/relay.rb5
-rw-r--r--app/models/remote_follow.rb2
-rw-r--r--app/models/status.rb2
-rw-r--r--app/models/tag.rb13
-rw-r--r--app/models/user.rb11
-rw-r--r--app/models/web/push_subscription.rb4
-rw-r--r--app/presenters/instance_presenter.rb4
-rw-r--r--app/serializers/activitypub/move_serializer.rb26
-rw-r--r--app/serializers/activitypub/note_serializer.rb12
-rw-r--r--app/serializers/nodeinfo/discovery_serializer.rb11
-rw-r--r--app/serializers/nodeinfo/serializer.rb41
-rw-r--r--app/serializers/rest/credential_account_serializer.rb1
-rw-r--r--app/serializers/rest/poll_serializer.rb7
-rw-r--r--app/serializers/rss/account_serializer.rb2
-rw-r--r--app/serializers/rss/tag_serializer.rb2
-rw-r--r--app/services/account_search_service.rb10
-rw-r--r--app/services/activitypub/process_poll_service.rb5
-rw-r--r--app/services/fetch_link_card_service.rb2
-rw-r--r--app/services/follow_service.rb2
-rw-r--r--app/services/move_service.rb32
-rw-r--r--app/services/post_status_service.rb2
-rw-r--r--app/services/search_service.rb3
-rw-r--r--app/services/tag_search_service.rb29
-rw-r--r--app/services/update_account_service.rb4
-rw-r--r--app/services/vote_service.rb32
-rw-r--r--app/views/about/_domain_blocks.html.haml10
-rw-r--r--app/views/about/blocks.html.haml48
-rw-r--r--app/views/about/more.html.haml55
-rw-r--r--app/views/admin/dashboard/index.html.haml24
-rw-r--r--app/views/admin/tags/index.html.haml8
-rw-r--r--app/views/auth/challenges/new.html.haml15
-rw-r--r--app/views/auth/registrations/_status.html.haml30
-rw-r--r--app/views/auth/registrations/edit.html.haml13
-rw-r--r--app/views/auth/shared/_links.html.haml2
-rw-r--r--app/views/layouts/admin.html.haml20
-rw-r--r--app/views/settings/aliases/index.html.haml29
-rw-r--r--app/views/settings/exports/show.html.haml4
-rw-r--r--app/views/settings/migration/redirects/new.html.haml27
-rw-r--r--app/views/settings/migrations/show.html.haml88
-rw-r--r--app/views/settings/profiles/show.html.haml5
-rw-r--r--app/views/settings/two_factor_authentications/show.html.haml38
-rw-r--r--app/views/statuses/_poll.html.haml16
-rw-r--r--app/views/user_mailer/two_factor_disabled.html.haml43
-rw-r--r--app/views/user_mailer/two_factor_disabled.text.erb7
-rw-r--r--app/views/user_mailer/two_factor_enabled.html.haml43
-rw-r--r--app/views/user_mailer/two_factor_enabled.text.erb7
-rw-r--r--app/views/user_mailer/two_factor_recovery_codes_changed.html.haml43
-rw-r--r--app/views/user_mailer/two_factor_recovery_codes_changed.text.erb7
-rw-r--r--app/workers/activitypub/move_distribution_worker.rb33
-rw-r--r--app/workers/move_worker.rb33
212 files changed, 4040 insertions, 1040 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 7b0438127..5d5db937c 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -4,9 +4,7 @@ class AboutController < ApplicationController
   before_action :set_pack
   layout 'public'
 
-  before_action :require_open_federation!, only: [:show, :more, :blocks]
-  before_action :check_blocklist_enabled, only: [:blocks]
-  before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
+  before_action :require_open_federation!, only: [:show, :more]
   before_action :set_body_classes, only: :show
   before_action :set_instance_presenter
   before_action :set_expires_in, only: [:show, :more, :terms]
@@ -17,15 +15,20 @@ class AboutController < ApplicationController
 
   def more
     flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
+
+    toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
+
+    @contents          = toc_generator.html
+    @table_of_contents = toc_generator.toc
+    @blocks            = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
   end
 
   def terms; end
 
-  def blocks
-    @show_rationale = Setting.show_domain_blocks_rationale == 'all'
-    @show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional?
-    @blocks = DomainBlock.with_user_facing_limitations.order('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain').to_a
-  end
+  helper_method :display_blocks?
+  helper_method :display_blocks_rationale?
+  helper_method :public_fetch_mode?
+  helper_method :new_user
 
   private
 
@@ -33,28 +36,14 @@ class AboutController < ApplicationController
     not_found if whitelist_mode?
   end
 
-  def check_blocklist_enabled
-    not_found if Setting.show_domain_blocks == 'disabled'
-  end
-
-  def blocklist_account_required?
-    Setting.show_domain_blocks == 'users'
+  def display_blocks?
+    Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
   end
 
-  def block_severity_text(block)
-    if block.severity == 'suspend'
-      I18n.t('domain_blocks.suspension')
-    else
-      limitations = []
-      limitations << I18n.t('domain_blocks.media_block') if block.reject_media?
-      limitations << I18n.t('domain_blocks.silence') if block.severity == 'silence'
-      limitations.join(', ')
-    end
+  def display_blocks_rationale?
+    Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
   end
 
-  helper_method :block_severity_text
-  helper_method :public_fetch_mode?
-
   def new_user
     User.new.tap do |user|
       user.build_account
@@ -62,8 +51,6 @@ class AboutController < ApplicationController
     end
   end
 
-  helper_method :new_user
-
   def set_pack
     use_pack 'public'
   end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 817e5e832..1c36813d0 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -10,6 +10,7 @@ class AccountsController < ApplicationController
   before_action :set_body_classes
 
   skip_around_action :set_locale, if: -> { request.format == :json }
+  skip_before_action :require_functional!
 
   def show
     respond_to do |format|
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index 989fee385..910fefb1c 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -33,9 +33,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
   def scope_for_collection
     case params[:id]
     when 'featured'
-      @account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
-        scope.merge!(@account.pinned_statuses)
-      end
+      return Status.none if @account.blocking?(signed_request_account)
+
+      @account.pinned_statuses
     else
       raise ActiveRecord::RecordNotFound
     end
diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb
index 1b02d3c36..6fbb6e063 100644
--- a/app/controllers/admin/relays_controller.rb
+++ b/app/controllers/admin/relays_controller.rb
@@ -3,6 +3,7 @@
 module Admin
   class RelaysController < BaseController
     before_action :set_relay, except: [:index, :new, :create]
+    before_action :require_signatures_enabled!, only: [:new, :create, :enable]
 
     def index
       authorize :relay, :update?
@@ -11,7 +12,7 @@ module Admin
 
     def new
       authorize :relay, :update?
-      @relay = Relay.new(inbox_url: Relay::PRESET_RELAY)
+      @relay = Relay.new
     end
 
     def create
@@ -54,5 +55,9 @@ module Admin
     def resource_params
       params.require(:relay).permit(:inbox_url)
     end
+
+    def require_signatures_enabled!
+      redirect_to admin_relays_path, alert: I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
+    end
   end
 end
diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb
index 2577a4b17..0652c3a7a 100644
--- a/app/controllers/admin/two_factor_authentications_controller.rb
+++ b/app/controllers/admin/two_factor_authentications_controller.rb
@@ -8,6 +8,7 @@ module Admin
       authorize @user, :disable_2fa?
       @user.disable_two_factor!
       log_action :disable_2fa, @user
+      UserMailer.two_factor_disabled(@user).deliver_later!
       redirect_to admin_accounts_path
     end
 
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 0787cd636..333db9618 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -57,6 +57,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
   end
 
   def pinned_scope
+    return Status.none if @account.blocking?(current_account)
+
     @account.pinned_statuses
   end
 
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index b306e8e8c..c12e1c12e 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -33,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController
   def follow
     FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
 
-    options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
+    options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
 
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
   end
diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb
index 7fdc030e5..76decdb25 100644
--- a/app/controllers/api/v2/search_controller.rb
+++ b/app/controllers/api/v2/search_controller.rb
@@ -22,7 +22,7 @@ class Api::V2::SearchController < Api::BaseController
       params[:q],
       current_account,
       limit_param(RESULTS_LIMIT),
-      search_params.merge(resolve: truthy_param?(:resolve))
+      search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
     )
   end
 
diff --git a/app/controllers/auth/challenges_controller.rb b/app/controllers/auth/challenges_controller.rb
new file mode 100644
index 000000000..060944240
--- /dev/null
+++ b/app/controllers/auth/challenges_controller.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Auth::ChallengesController < ApplicationController
+  include ChallengableConcern
+
+  layout 'auth'
+
+  before_action :authenticate_user!
+
+  skip_before_action :require_functional!
+
+  def create
+    if challenge_passed?
+      session[:challenge_passed_at] = Time.now.utc
+      redirect_to challenge_params[:return_to]
+    else
+      @challenge = Form::Challenge.new(return_to: challenge_params[:return_to])
+      flash.now[:alert] = I18n.t('challenge.invalid_password')
+      render_challenge
+    end
+  end
+end
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index c2b38883b..efde02ac2 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -9,6 +9,7 @@ class Auth::SessionsController < Devise::SessionsController
   skip_before_action :require_functional!
 
   prepend_before_action :set_pack
+  prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
 
   before_action :set_instance_presenter, only: [:new]
   before_action :set_body_classes
@@ -22,34 +23,32 @@ class Auth::SessionsController < Devise::SessionsController
   end
 
   def create
-    self.resource = begin
-      if user_params[:email].blank? && session[:otp_user_id].present?
-        User.find(session[:otp_user_id])
-      else
-        warden.authenticate!(auth_options)
-      end
-    end
-
-    if resource.otp_required_for_login?
-      if user_params[:otp_attempt].present? && session[:otp_user_id].present?
-        authenticate_with_two_factor_via_otp(resource)
-      else
-        prompt_for_two_factor(resource)
-      end
-    else
-      authenticate_and_respond(resource)
+    super do |resource|
+      remember_me(resource)
+      flash.delete(:notice)
     end
   end
 
   def destroy
     tmp_stored_location = stored_location_for(:user)
     super
+    session.delete(:challenge_passed_at)
     flash.delete(:notice)
     store_location_for(:user, tmp_stored_location) if continue_after?
   end
 
   protected
 
+  def find_user
+    if session[:otp_user_id]
+      User.find(session[:otp_user_id])
+    else
+      user   = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
+      user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
+      user ||= User.find_for_authentication(email: user_params[:email])
+    end
+  end
+
   def user_params
     params.require(:user).permit(:email, :password, :otp_attempt)
   end
@@ -72,6 +71,10 @@ class Auth::SessionsController < Devise::SessionsController
     super
   end
 
+  def two_factor_enabled?
+    find_user&.otp_required_for_login?
+  end
+
   def valid_otp_attempt?(user)
     user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
       user.invalidate_otp_backup_code!(user_params[:otp_attempt])
@@ -79,10 +82,24 @@ class Auth::SessionsController < Devise::SessionsController
     false
   end
 
+  def authenticate_with_two_factor
+    user = self.resource = find_user
+
+    if user_params[:otp_attempt].present? && session[:otp_user_id]
+      authenticate_with_two_factor_via_otp(user)
+    elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
+      # If encrypted_password is blank, we got the user from LDAP or PAM,
+      # so credentials are already valid
+
+      prompt_for_two_factor(user)
+    end
+  end
+
   def authenticate_with_two_factor_via_otp(user)
     if valid_otp_attempt?(user)
       session.delete(:otp_user_id)
-      authenticate_and_respond(user)
+      remember_me(user)
+      sign_in(user)
     else
       flash.now[:alert] = I18n.t('users.invalid_otp_token')
       prompt_for_two_factor(user)
@@ -91,16 +108,10 @@ class Auth::SessionsController < Devise::SessionsController
 
   def prompt_for_two_factor(user)
     session[:otp_user_id] = user.id
+    @body_classes = 'lighter'
     render :two_factor
   end
 
-  def authenticate_and_respond(user)
-    sign_in(user)
-    remember_me(user)
-
-    respond_with user, location: after_sign_in_path_for(user)
-  end
-
   private
 
   def set_pack
@@ -117,11 +128,9 @@ class Auth::SessionsController < Devise::SessionsController
 
   def home_paths(resource)
     paths = [about_path]
-
     if single_user_mode? && resource.is_a?(User)
       paths << short_account_path(username: resource.account)
     end
-
     paths
   end
 
diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb
new file mode 100644
index 000000000..b29d90b3c
--- /dev/null
+++ b/app/controllers/concerns/challengable_concern.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+# This concern is inspired by "sudo mode" on GitHub. It
+# is a way to re-authenticate a user before allowing them
+# to see or perform an action.
+#
+# Add `before_action :require_challenge!` to actions you
+# want to protect.
+#
+# The user will be shown a page to enter the challenge (which
+# is either the password, or just the username when no
+# password exists). Upon passing, there is a grace period
+# during which no challenge will be asked from the user.
+#
+# Accessing challenge-protected resources during the grace
+# period will refresh the grace period.
+module ChallengableConcern
+  extend ActiveSupport::Concern
+
+  CHALLENGE_TIMEOUT = 1.hour.freeze
+
+  def require_challenge!
+    return if skip_challenge?
+
+    if challenge_passed_recently?
+      session[:challenge_passed_at] = Time.now.utc
+      return
+    end
+
+    @challenge = Form::Challenge.new(return_to: request.url)
+
+    if params.key?(:form_challenge)
+      if challenge_passed?
+        session[:challenge_passed_at] = Time.now.utc
+        return
+      else
+        flash.now[:alert] = I18n.t('challenge.invalid_password')
+        render_challenge
+      end
+    else
+      render_challenge
+    end
+  end
+
+  def render_challenge
+    @body_classes = 'lighter'
+    render template: 'auth/challenges/new', layout: 'auth'
+  end
+
+  def challenge_passed?
+    current_user.valid_password?(challenge_params[:current_password])
+  end
+
+  def skip_challenge?
+    current_user.encrypted_password.blank?
+  end
+
+  def challenge_passed_recently?
+    session[:challenge_passed_at].present? && session[:challenge_passed_at] >= CHALLENGE_TIMEOUT.ago
+  end
+
+  def challenge_params
+    params.require(:form_challenge).permit(:current_password, :return_to)
+  end
+end
diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb
index e20b71a30..bfe990c82 100644
--- a/app/controllers/concerns/export_controller_concern.rb
+++ b/app/controllers/concerns/export_controller_concern.rb
@@ -5,7 +5,10 @@ module ExportControllerConcern
 
   included do
     before_action :authenticate_user!
+    before_action :require_not_suspended!
     before_action :load_export
+
+    skip_before_action :require_functional!
   end
 
   private
@@ -27,4 +30,8 @@ module ExportControllerConcern
   def export_filename
     "#{controller_name}.csv"
   end
+
+  def require_not_suspended!
+    forbidden if current_account.suspended?
+  end
 end
diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb
index e3f67bd14..0a667a6a6 100644
--- a/app/controllers/custom_css_controller.rb
+++ b/app/controllers/custom_css_controller.rb
@@ -2,6 +2,7 @@
 
 class CustomCssController < ApplicationController
   skip_before_action :store_current_location
+  skip_before_action :require_functional!
 
   before_action :set_cache_headers
 
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
index bbfdde8af..adf2bd014 100644
--- a/app/controllers/directories_controller.rb
+++ b/app/controllers/directories_controller.rb
@@ -10,6 +10,8 @@ class DirectoriesController < ApplicationController
   before_action :set_accounts
   before_action :set_pack
 
+  skip_before_action :require_functional!
+
   def index
     render :index
   end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 4641a8bb9..df46f5f72 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -8,6 +8,7 @@ class FollowerAccountsController < ApplicationController
   before_action :set_cache_headers
 
   skip_around_action :set_locale, if: -> { request.format == :json }
+  skip_before_action :require_functional!
 
   def index
     respond_to do |format|
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 6e80554fb..8cab67ff5 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -8,6 +8,7 @@ class FollowingAccountsController < ApplicationController
   before_action :set_cache_headers
 
   skip_around_action :set_locale, if: -> { request.format == :json }
+  skip_before_action :require_functional!
 
   def index
     respond_to do |format|
diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb
index 491cde745..960510f60 100644
--- a/app/controllers/manifests_controller.rb
+++ b/app/controllers/manifests_controller.rb
@@ -2,6 +2,7 @@
 
 class ManifestsController < ApplicationController
   skip_before_action :store_current_location
+  skip_before_action :require_functional!
 
   def show
     expires_in 3.minutes, public: true
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 1f693de32..05cf09c28 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -4,6 +4,7 @@ class MediaController < ApplicationController
   include Authorization
 
   skip_before_action :store_current_location
+  skip_before_action :require_functional!
 
   before_action :authenticate_user!, if: :whitelist_mode?
   before_action :set_media_attachment
diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb
index 47544f21c..014b89de1 100644
--- a/app/controllers/media_proxy_controller.rb
+++ b/app/controllers/media_proxy_controller.rb
@@ -4,6 +4,7 @@ class MediaProxyController < ApplicationController
   include RoutingHelper
 
   skip_before_action :store_current_location
+  skip_before_action :require_functional!
 
   before_action :authenticate_user!, if: :whitelist_mode?
 
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 65dfa35db..93a0a7476 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -8,6 +8,8 @@ class RemoteFollowController < ApplicationController
   before_action :set_pack
   before_action :set_body_classes
 
+  skip_before_action :require_functional!
+
   def new
     @remote_follow = RemoteFollow.new(session_params)
   end
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index 6b797b10f..e058d0ed5 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -11,6 +11,8 @@ class RemoteInteractionController < ApplicationController
   before_action :set_body_classes
   before_action :set_pack
 
+  skip_before_action :require_functional!
+
   def new
     @remote_follow = RemoteFollow.new(session_params)
   end
diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb
new file mode 100644
index 000000000..b7c9a409d
--- /dev/null
+++ b/app/controllers/settings/aliases_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class Settings::AliasesController < Settings::BaseController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :set_aliases, except: :destroy
+  before_action :set_alias, only: :destroy
+
+  def index
+    @alias = current_account.aliases.build
+  end
+
+  def create
+    @alias = current_account.aliases.build(resource_params)
+
+    if @alias.save
+      ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
+      redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg')
+    else
+      render :index
+    end
+  end
+
+  def destroy
+    @alias.destroy!
+    redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg')
+  end
+
+  private
+
+  def resource_params
+    params.require(:account_alias).permit(:acct)
+  end
+
+  def set_alias
+    @alias = current_account.aliases.find(params[:id])
+  end
+
+  def set_aliases
+    @aliases = current_account.aliases.order(id: :desc).reject(&:new_record?)
+  end
+end
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index 3012fbf77..0e93d07a9 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController
   layout 'admin'
 
   before_action :authenticate_user!
+  before_action :require_not_suspended!
+
+  skip_before_action :require_functional!
 
   def show
     @export  = Export.new(current_account)
@@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController
   def lock_options
     { redis: Redis.current, key: "backup:#{current_user.id}" }
   end
+
+  def require_not_suspended!
+    forbidden if current_account.suspended?
+  end
 end
diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb
new file mode 100644
index 000000000..6e5b72ffb
--- /dev/null
+++ b/app/controllers/settings/migration/redirects_controller.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class Settings::Migration::RedirectsController < Settings::BaseController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :require_not_suspended!
+
+  skip_before_action :require_functional!
+
+  def new
+    @redirect = Form::Redirect.new
+  end
+
+  def create
+    @redirect = Form::Redirect.new(resource_params.merge(account: current_account))
+
+    if @redirect.valid_with_challenge?(current_user)
+      current_account.update!(moved_to_account: @redirect.target_account)
+      ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
+      redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
+    else
+      render :new
+    end
+  end
+
+  def destroy
+    if current_account.moved_to_account_id.present?
+      current_account.update!(moved_to_account: nil)
+      ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
+    end
+
+    redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg')
+  end
+
+  private
+
+  def resource_params
+    params.require(:form_redirect).permit(:acct, :current_password, :current_username)
+  end
+
+  def require_not_suspended!
+    forbidden if current_account.suspended?
+  end
+end
diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb
index 59eb48779..68304bb51 100644
--- a/app/controllers/settings/migrations_controller.rb
+++ b/app/controllers/settings/migrations_controller.rb
@@ -4,31 +4,48 @@ class Settings::MigrationsController < Settings::BaseController
   layout 'admin'
 
   before_action :authenticate_user!
+  before_action :require_not_suspended!
+  before_action :set_migrations
+  before_action :set_cooldown
+
+  skip_before_action :require_functional!
 
   def show
-    @migration = Form::Migration.new(account: current_account.moved_to_account)
+    @migration = current_account.migrations.build
   end
 
-  def update
-    @migration = Form::Migration.new(resource_params)
+  def create
+    @migration = current_account.migrations.build(resource_params)
 
-    if @migration.valid? && migration_account_changed?
-      current_account.update!(moved_to_account: @migration.account)
-      ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
-      redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
+    if @migration.save_with_challenge(current_user)
+      MoveService.new.call(@migration)
+      redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
     else
       render :show
     end
   end
 
+  helper_method :on_cooldown?
+
   private
 
   def resource_params
-    params.require(:migration).permit(:acct)
+    params.require(:account_migration).permit(:acct, :current_password, :current_username)
+  end
+
+  def set_migrations
+    @migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?)
+  end
+
+  def set_cooldown
+    @cooldown = current_account.migrations.within_cooldown.first
+  end
+
+  def on_cooldown?
+    @cooldown.present?
   end
 
-  def migration_account_changed?
-    current_account.moved_to_account_id != @migration.account&.id &&
-      current_account.id != @migration.account&.id
+  def require_not_suspended!
+    forbidden if current_account.suspended?
   end
 end
diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb
index 46c90bf74..ef4df3339 100644
--- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb
+++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb
@@ -3,9 +3,12 @@
 module Settings
   module TwoFactorAuthentication
     class ConfirmationsController < BaseController
+      include ChallengableConcern
+
       layout 'admin'
 
       before_action :authenticate_user!
+      before_action :require_challenge!
       before_action :ensure_otp_secret
 
       skip_before_action :require_functional!
@@ -22,6 +25,8 @@ module Settings
           @recovery_codes = current_user.generate_otp_backup_codes!
           current_user.save!
 
+          UserMailer.two_factor_enabled(current_user).deliver_later!
+
           render 'settings/two_factor_authentication/recovery_codes/index'
         else
           flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb
index 09a759860..0c4f5bff7 100644
--- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb
+++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb
@@ -3,16 +3,22 @@
 module Settings
   module TwoFactorAuthentication
     class RecoveryCodesController < BaseController
+      include ChallengableConcern
+
       layout 'admin'
 
       before_action :authenticate_user!
+      before_action :require_challenge!, on: :create
 
       skip_before_action :require_functional!
 
       def create
         @recovery_codes = current_user.generate_otp_backup_codes!
         current_user.save!
+
+        UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later!
         flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
+
         render :index
       end
     end
diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb
index c93b17577..9118a7933 100644
--- a/app/controllers/settings/two_factor_authentications_controller.rb
+++ b/app/controllers/settings/two_factor_authentications_controller.rb
@@ -2,10 +2,13 @@
 
 module Settings
   class TwoFactorAuthenticationsController < BaseController
+    include ChallengableConcern
+
     layout 'admin'
 
     before_action :authenticate_user!
     before_action :verify_otp_required, only: [:create]
+    before_action :require_challenge!, only: [:create]
 
     skip_before_action :require_functional!
 
@@ -23,6 +26,7 @@ module Settings
       if acceptable_code?
         current_user.otp_required_for_login = false
         current_user.save!
+        UserMailer.two_factor_disabled(current_user).deliver_later!
         redirect_to settings_two_factor_authentication_path
       else
         flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 83c412d5c..1b00d38c9 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -19,6 +19,7 @@ class StatusesController < ApplicationController
   before_action :set_autoplay, only: :embed
 
   skip_around_action :set_locale, if: -> { request.format == :json }
+  skip_before_action :require_functional!, only: [:show, :embed]
 
   content_security_policy only: :embed do |p|
     p.frame_ancestors(false)
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index c447a3a2b..ef61c980f 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -13,6 +13,8 @@ class TagsController < ApplicationController
   before_action :set_body_classes
   before_action :set_instance_presenter
 
+  skip_before_action :require_functional!
+
   def show
     respond_to do |format|
       format.html do
diff --git a/app/controllers/well_known/nodeinfo_controller.rb b/app/controllers/well_known/nodeinfo_controller.rb
new file mode 100644
index 000000000..11a699ebc
--- /dev/null
+++ b/app/controllers/well_known/nodeinfo_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module WellKnown
+  class NodeInfoController < ActionController::Base
+    include CacheConcern
+
+    before_action { response.headers['Vary'] = 'Accept' }
+
+    def index
+      expires_in 3.days, public: true
+      render_with_cache json: {}, serializer: NodeInfo::DiscoverySerializer, adapter: NodeInfo::Adapter, expires_in: 3.days, root: 'nodeinfo'
+    end
+
+    def show
+      expires_in 30.minutes, public: true
+      render_with_cache json: {}, serializer: NodeInfo::Serializer, adapter: NodeInfo::Adapter, expires_in: 30.minutes, root: 'nodeinfo'
+    end
+  end
+end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 2b3fd1263..ecc73baf5 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -87,4 +87,12 @@ module SettingsHelper
       'desktop'
     end
   end
+
+  def compact_account_link_to(account)
+    return if account.nil?
+
+    link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
+      safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
+    end
+  end
 end
diff --git a/app/javascript/mastodon/actions/blocks.js b/app/javascript/mastodon/actions/blocks.js
index 7000f5a71..fd9881302 100644
--- a/app/javascript/mastodon/actions/blocks.js
+++ b/app/javascript/mastodon/actions/blocks.js
@@ -1,6 +1,7 @@
 import api, { getLinks } from '../api';
 import { fetchRelationships } from './accounts';
 import { importFetchedAccounts } from './importer';
+import { openModal } from './modal';
 
 export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
 export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
@@ -10,6 +11,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
 export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
 export const BLOCKS_EXPAND_FAIL    = 'BLOCKS_EXPAND_FAIL';
 
+export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
+
 export function fetchBlocks() {
   return (dispatch, getState) => {
     dispatch(fetchBlocksRequest());
@@ -83,3 +86,14 @@ export function expandBlocksFail(error) {
     error,
   };
 };
+
+export function initBlockModal(account) {
+  return dispatch => {
+    dispatch({
+      type: BLOCKS_INIT_MODAL,
+      account,
+    });
+
+    dispatch(openModal('BLOCK'));
+  };
+}
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 061a36bb8..8e7906c73 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -234,7 +234,7 @@ export function uploadCompose(files) {
             progress[i] = loaded;
             dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
           },
-        }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
+        }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
       }).catch(error => dispatch(uploadComposeFail(error)));
     };
   };
@@ -289,10 +289,11 @@ export function uploadComposeProgress(loaded, total) {
   };
 };
 
-export function uploadComposeSuccess(media) {
+export function uploadComposeSuccess(media, file) {
   return {
     type: COMPOSE_UPLOAD_SUCCESS,
     media: media,
+    file: file,
     skipLoading: true,
   };
 };
@@ -368,6 +369,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
       q: token.slice(1),
       resolve: false,
       limit: 4,
+      exclude_unreviewed: true,
     },
   }).then(({ data }) => {
     dispatch(readyComposeSuggestionsTags(token, data.hashtags));
diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js
index c6e062ef7..4ef654b1f 100644
--- a/app/javascript/mastodon/actions/conversations.js
+++ b/app/javascript/mastodon/actions/conversations.js
@@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE        = 'CONVERSATIONS_UPDATE';
 
 export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
 
+export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
+export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
+export const CONVERSATIONS_DELETE_FAIL    = 'CONVERSATIONS_DELETE_FAIL';
+
 export const mountConversations = () => ({
   type: CONVERSATIONS_MOUNT,
 });
@@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => {
     conversation,
   });
 };
+
+export const deleteConversation = conversationId => (dispatch, getState) => {
+  dispatch(deleteConversationRequest(conversationId));
+
+  api(getState).delete(`/api/v1/conversations/${conversationId}`)
+    .then(() => dispatch(deleteConversationSuccess(conversationId)))
+    .catch(error => dispatch(deleteConversationFail(conversationId, error)));
+};
+
+export const deleteConversationRequest = id => ({
+  type: CONVERSATIONS_DELETE_REQUEST,
+  id,
+});
+
+export const deleteConversationSuccess = id => ({
+  type: CONVERSATIONS_DELETE_SUCCESS,
+  id,
+});
+
+export const deleteConversationFail = (id, error) => ({
+  type: CONVERSATIONS_DELETE_FAIL,
+  id,
+  error,
+});
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 5e7e78e69..f7108fdb9 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -73,8 +73,9 @@ export function normalizePoll(poll) {
 
   const emojiMap = makeEmojiMap(normalPoll);
 
-  normalPoll.options = poll.options.map(option => ({
+  normalPoll.options = poll.options.map((option, index) => ({
     ...option,
+    voted: poll.own_votes && poll.own_votes.includes(index),
     title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
   }));
 
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index ea76255e3..58803d1ae 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -28,6 +28,9 @@ export const NOTIFICATIONS_CLEAR        = 'NOTIFICATIONS_CLEAR';
 export const NOTIFICATIONS_SCROLL_TOP   = 'NOTIFICATIONS_SCROLL_TOP';
 export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
 
+export const NOTIFICATIONS_MOUNT   = 'NOTIFICATIONS_MOUNT';
+export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
+
 defineMessages({
   mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
   group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
@@ -215,3 +218,11 @@ export function setFilter (filterType) {
     dispatch(saveSettings());
   };
 };
+
+export const mountNotifications = () => ({
+  type: NOTIFICATIONS_MOUNT,
+});
+
+export const unmountNotifications = () => ({
+  type: NOTIFICATIONS_UNMOUNT,
+});
diff --git a/app/javascript/mastodon/components/avatar_composite.js b/app/javascript/mastodon/components/avatar_composite.js
index 4a9a73c51..5d5b89749 100644
--- a/app/javascript/mastodon/components/avatar_composite.js
+++ b/app/javascript/mastodon/components/avatar_composite.js
@@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent {
 
     if (size === 2) {
       if (index === 0) {
-        right = '2px';
+        right = '1px';
       } else {
-        left = '2px';
+        left = '1px';
       }
     } else if (size === 3) {
       if (index === 0) {
-        right = '2px';
+        right = '1px';
       } else if (index > 0) {
-        left = '2px';
+        left = '1px';
       }
 
       if (index === 1) {
-        bottom = '2px';
+        bottom = '1px';
       } else if (index > 1) {
-        top = '2px';
+        top = '1px';
       }
     } else if (size === 4) {
       if (index === 0 || index === 2) {
-        right = '2px';
+        right = '1px';
       }
 
       if (index === 1 || index === 3) {
-        left = '2px';
+        left = '1px';
       }
 
       if (index < 2) {
-        bottom = '2px';
+        bottom = '1px';
       } else {
-        top = '2px';
+        top = '1px';
       }
     }
 
@@ -88,7 +88,13 @@ export default class AvatarComposite extends React.PureComponent {
 
     return (
       <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
-        {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
+        {accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
+
+        {accounts.size > 4 && (
+          <span className='account__avatar-composite__label'>
+            +{accounts.size - 4}
+          </span>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index 8a26742b5..0038995c8 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -120,7 +120,7 @@ class ColumnHeader extends React.PureComponent {
           <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
         </div>
       );
-    } else if (multiColumn) {
+    } else if (multiColumn && this.props.onPin) {
       pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
     }
 
@@ -142,7 +142,7 @@ class ColumnHeader extends React.PureComponent {
       collapsedContent.push(pinButton);
     }
 
-    if (children || multiColumn) {
+    if (children || (multiColumn && this.props.onPin)) {
       collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
     }
 
diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js
index 373f710d3..cdbcf8f70 100644
--- a/app/javascript/mastodon/components/poll.js
+++ b/app/javascript/mastodon/components/poll.js
@@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring';
 import escapeTextContentForBrowser from 'escape-html';
 import emojify from 'mastodon/features/emoji/emoji';
 import RelativeTimestamp from './relative_timestamp';
+import Icon from 'mastodon/components/icon';
 
 const messages = defineMessages({
   closed: { id: 'poll.closed', defaultMessage: 'Closed' },
+  voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
 });
 
 const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
@@ -99,10 +101,12 @@ class Poll extends ImmutablePureComponent {
   };
 
   renderOption (option, optionIndex, showResults) {
-    const { poll, disabled } = this.props;
-    const percent            = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
-    const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
-    const active             = !!this.state.selected[`${optionIndex}`];
+    const { poll, disabled, intl } = this.props;
+    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');
+    const percent         = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
+    const leading         = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
+    const active          = !!this.state.selected[`${optionIndex}`];
+    const voted           = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
 
     let titleEmojified = option.get('title_emojified');
     if (!titleEmojified) {
@@ -131,7 +135,10 @@ class Poll extends ImmutablePureComponent {
           />
 
           {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
-          {showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
+          {showResults && <span className='poll__number'>
+            {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
+            {Math.round(percent)}%
+          </span>}
 
           <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
         </label>
@@ -151,6 +158,14 @@ class Poll extends ImmutablePureComponent {
     const showResults   = poll.get('voted') || expired;
     const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
 
+    let votesCount = null;
+
+    if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
+      votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
+    } else {
+      votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
+    }
+
     return (
       <div className='poll'>
         <ul>
@@ -160,7 +175,7 @@ class Poll extends ImmutablePureComponent {
         <div className='poll__footer'>
           {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
           {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
-          <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />
+          {votesCount}
           {poll.get('expires_at') && <span> · {timeRemaining}</span>}
         </div>
       </div>
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 253646ed0..421756803 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -199,6 +199,7 @@ export default class ScrollableList extends PureComponent {
     this.clearMouseIdleTimer();
     this.detachScrollListener();
     this.detachIntersectionObserver();
+
     detachFullscreenListener(this.onFullScreenChange);
   }
 
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index db340032a..ba55ecbc7 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -2,17 +2,17 @@ import React, { PureComponent, Fragment } from 'react';
 import ReactDOM from 'react-dom';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import MediaGallery from '../components/media_gallery';
-import Video from '../features/video';
-import Card from '../features/status/components/card';
+import { List as ImmutableList, fromJS } from 'immutable';
+import { getLocale } from 'mastodon/locales';
+import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
+import MediaGallery from 'mastodon/components/media_gallery';
 import Poll from 'mastodon/components/poll';
 import Hashtag from 'mastodon/components/hashtag';
+import ModalRoot from 'mastodon/components/modal_root';
+import MediaModal from 'mastodon/features/ui/components/media_modal';
+import Video from 'mastodon/features/video';
+import Card from 'mastodon/features/status/components/card';
 import Audio from 'mastodon/features/audio';
-import ModalRoot from '../components/modal_root';
-import { getScrollbarWidth } from '../features/ui/components/modal_root';
-import MediaModal from '../features/ui/components/media_modal';
-import { List as ImmutableList, fromJS } from 'immutable';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index fa58589a6..fb22676e0 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -1,4 +1,3 @@
-import React from 'react';
 import { connect } from 'react-redux';
 import Status from '../components/status';
 import { makeGetStatus } from '../selectors';
@@ -15,7 +14,6 @@ import {
   pin,
   unpin,
 } from '../actions/interactions';
-import { blockAccount } from '../actions/accounts';
 import {
   muteStatus,
   unmuteStatus,
@@ -24,9 +22,10 @@ import {
   revealStatus,
 } from '../actions/statuses';
 import { initMuteModal } from '../actions/mutes';
+import { initBlockModal } from '../actions/blocks';
 import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import { boostModal, deleteModal } from '../initial_state';
 import { showAlertForError } from '../actions/alerts';
 
@@ -35,10 +34,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   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.' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   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?' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
 });
 
 const makeMapStateToProps = () => {
@@ -56,6 +53,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   onReply (status, router) {
     dispatch((_, getState) => {
       let state = getState();
+
       if (state.getIn(['compose', 'text']).trim().length !== 0) {
         dispatch(openModal('CONFIRM', {
           message: intl.formatMessage(messages.replyMessage),
@@ -137,16 +135,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onBlock (status) {
     const account = status.get('account');
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.blockConfirm),
-      onConfirm: () => dispatch(blockAccount(account.get('id'))),
-      secondary: intl.formatMessage(messages.blockAndReport),
-      onSecondary: () => {
-        dispatch(blockAccount(account.get('id')));
-        dispatch(initReport(account, status));
-      },
-    }));
+    dispatch(initBlockModal(account));
   },
 
   onReport (status) {
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index 4d4ae6e82..8728b4806 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -5,7 +5,6 @@ import Header from '../components/header';
 import {
   followAccount,
   unfollowAccount,
-  blockAccount,
   unblockAccount,
   unmuteAccount,
   pinAccount,
@@ -16,6 +15,7 @@ import {
   directCompose,
 } from '../../../actions/compose';
 import { initMuteModal } from '../../../actions/mutes';
+import { initBlockModal } from '../../../actions/blocks';
 import { initReport } from '../../../actions/reports';
 import { openModal } from '../../../actions/modal';
 import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
@@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
 });
 
 const makeMapStateToProps = () => {
@@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     if (account.getIn(['relationship', 'blocking'])) {
       dispatch(unblockAccount(account.get('id')));
     } else {
-      dispatch(openModal('CONFIRM', {
-        message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-        confirm: intl.formatMessage(messages.blockConfirm),
-        onConfirm: () => dispatch(blockAccount(account.get('id'))),
-        secondary: intl.formatMessage(messages.blockAndReport),
-        onSecondary: () => {
-          dispatch(blockAccount(account.get('id')));
-          dispatch(initReport(account));
-        },
-      }));
+      dispatch(initBlockModal(account));
     }
   },
 
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
index ffcd6d281..cc3faf0de 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
@@ -2,9 +2,28 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import StatusContainer from '../../../containers/status_container';
+import StatusContent from 'mastodon/components/status_content';
+import AttachmentList from 'mastodon/components/attachment_list';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
+import AvatarComposite from 'mastodon/components/avatar_composite';
+import Permalink from 'mastodon/components/permalink';
+import IconButton from 'mastodon/components/icon_button';
+import RelativeTimestamp from 'mastodon/components/relative_timestamp';
+import { HotKeys } from 'react-hotkeys';
 
-export default class Conversation extends ImmutablePureComponent {
+const messages = defineMessages({
+  more: { id: 'status.more', defaultMessage: 'More' },
+  open: { id: 'conversation.open', defaultMessage: 'View conversation' },
+  reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
+  delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
+  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
+  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+});
+
+export default @injectIntl
+class Conversation extends ImmutablePureComponent {
 
   static contextTypes = {
     router: PropTypes.object,
@@ -13,11 +32,12 @@ export default class Conversation extends ImmutablePureComponent {
   static propTypes = {
     conversationId: PropTypes.string.isRequired,
     accounts: ImmutablePropTypes.list.isRequired,
-    lastStatusId: PropTypes.string,
+    lastStatus: ImmutablePropTypes.map,
     unread:PropTypes.bool.isRequired,
     onMoveUp: PropTypes.func,
     onMoveDown: PropTypes.func,
     markRead: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
   };
 
   handleClick = () => {
@@ -25,13 +45,25 @@ export default class Conversation extends ImmutablePureComponent {
       return;
     }
 
-    const { lastStatusId, unread, markRead } = this.props;
+    const { lastStatus, unread, markRead } = this.props;
 
     if (unread) {
       markRead();
     }
 
-    this.context.router.history.push(`/statuses/${lastStatusId}`);
+    this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
+  }
+
+  handleMarkAsRead = () => {
+    this.props.markRead();
+  }
+
+  handleReply = () => {
+    this.props.reply(this.props.lastStatus, this.context.router.history);
+  }
+
+  handleDelete = () => {
+    this.props.delete();
   }
 
   handleHotkeyMoveUp = () => {
@@ -42,22 +74,88 @@ export default class Conversation extends ImmutablePureComponent {
     this.props.onMoveDown(this.props.conversationId);
   }
 
+  handleConversationMute = () => {
+    this.props.onMute(this.props.lastStatus);
+  }
+
+  handleShowMore = () => {
+    this.props.onToggleHidden(this.props.lastStatus);
+  }
+
   render () {
-    const { accounts, lastStatusId, unread } = this.props;
+    const { accounts, lastStatus, unread, intl } = this.props;
 
-    if (lastStatusId === null) {
+    if (lastStatus === null) {
       return null;
     }
 
+    const menu = [
+      { text: intl.formatMessage(messages.open), action: this.handleClick },
+      null,
+    ];
+
+    menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
+
+    if (unread) {
+      menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
+      menu.push(null);
+    }
+
+    menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
+
+    const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
+
+    const handlers = {
+      reply: this.handleReply,
+      open: this.handleClick,
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      toggleHidden: this.handleShowMore,
+    };
+
     return (
-      <StatusContainer
-        id={lastStatusId}
-        unread={unread}
-        otherAccounts={accounts}
-        onMoveUp={this.handleHotkeyMoveUp}
-        onMoveDown={this.handleHotkeyMoveDown}
-        onClick={this.handleClick}
-      />
+      <HotKeys handlers={handlers}>
+        <div className='conversation focusable muted' tabIndex='0'>
+          <div className='conversation__avatar'>
+            <AvatarComposite accounts={accounts} size={48} />
+          </div>
+
+          <div className='conversation__content'>
+            <div className='conversation__content__info'>
+              <div className='conversation__content__relative-time'>
+                <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
+              </div>
+
+              <div className='conversation__content__names'>
+                <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
+              </div>
+            </div>
+
+            <StatusContent
+              status={lastStatus}
+              onClick={this.handleClick}
+              expanded={!lastStatus.get('hidden')}
+              onExpandedToggle={this.handleShowMore}
+              collapsable
+            />
+
+            {lastStatus.get('media_attachments').size > 0 && (
+              <AttachmentList
+                compact
+                media={lastStatus.get('media_attachments')}
+              />
+            )}
+
+            <div className='status__action-bar'>
+              <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
+
+              <div className='status__action-bar-dropdown'>
+                <DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
+              </div>
+            </div>
+          </div>
+        </div>
+      </HotKeys>
     );
   }
 
diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js
index bd6f6bfb0..94cef81a7 100644
--- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js
+++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js
@@ -1,19 +1,74 @@
 import { connect } from 'react-redux';
 import Conversation from '../components/conversation';
-import { markConversationRead } from '../../../actions/conversations';
+import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
+import { makeGetStatus } from 'mastodon/selectors';
+import { replyCompose } from 'mastodon/actions/compose';
+import { openModal } from 'mastodon/actions/modal';
+import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'mastodon/actions/statuses';
+import { defineMessages, injectIntl } from 'react-intl';
 
-const mapStateToProps = (state, { conversationId }) => {
-  const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
+const messages = defineMessages({
+  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?' },
+});
+
+const mapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  return (state, { conversationId }) => {
+    const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
+    const lastStatusId = conversation.get('last_status', null);
 
-  return {
-    accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
-    unread: conversation.get('unread'),
-    lastStatusId: conversation.get('last_status', null),
+    return {
+      accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
+      unread: conversation.get('unread'),
+      lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
+    };
   };
 };
 
-const mapDispatchToProps = (dispatch, { conversationId }) => ({
-  markRead: () => dispatch(markConversationRead(conversationId)),
+const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
+
+  markRead () {
+    dispatch(markConversationRead(conversationId));
+  },
+
+  reply (status, router) {
+    dispatch((_, getState) => {
+      let state = getState();
+
+      if (state.getIn(['compose', 'text']).trim().length !== 0) {
+        dispatch(openModal('CONFIRM', {
+          message: intl.formatMessage(messages.replyMessage),
+          confirm: intl.formatMessage(messages.replyConfirm),
+          onConfirm: () => dispatch(replyCompose(status, router)),
+        }));
+      } else {
+        dispatch(replyCompose(status, router));
+      }
+    });
+  },
+
+  delete () {
+    dispatch(deleteConversation(conversationId));
+  },
+
+  onMute (status) {
+    if (status.get('muted')) {
+      dispatch(unmuteStatus(status.get('id')));
+    } else {
+      dispatch(muteStatus(status.get('id')));
+    }
+  },
+
+  onToggleHidden (status) {
+    if (status.get('hidden')) {
+      dispatch(revealStatus(status.get('id')));
+    } else {
+      dispatch(hideStatus(status.get('id')));
+    }
+  },
+
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation));
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index 359bb7ffd..cd10e20b7 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -99,4 +99,4 @@ export const buildCustomEmojis = (customEmojis) => {
   return emojis;
 };
 
-export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set());
+export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));
diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js
index 62d3c2f06..90c26f0c3 100644
--- a/app/javascript/mastodon/features/favourites/index.js
+++ b/app/javascript/mastodon/features/favourites/index.js
@@ -27,7 +27,9 @@ class Favourites extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchFavourites(this.props.params.statusId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchFavourites(this.props.params.statusId));
+    }
   }
 
   componentWillReceiveProps (nextProps) {
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index 3913bf8d0..9e635d250 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -40,8 +40,10 @@ class Followers extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(fetchFollowers(this.props.params.accountId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchAccount(this.props.params.accountId));
+      this.props.dispatch(fetchFollowers(this.props.params.accountId));
+    }
   }
 
   componentWillReceiveProps (nextProps) {
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index 8e126f4c3..284ae2c11 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -40,8 +40,10 @@ class Following extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(fetchFollowing(this.props.params.accountId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchAccount(this.props.params.accountId));
+      this.props.dispatch(fetchFollowing(this.props.params.accountId));
+    }
   }
 
   componentWillReceiveProps (nextProps) {
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index f6d90580b..67ec7665b 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -77,16 +77,14 @@ class GettingStarted extends ImmutablePureComponent {
   };
 
   componentDidMount () {
-    const { myAccount, fetchFollowRequests, multiColumn } = this.props;
+    const { fetchFollowRequests, multiColumn } = this.props;
 
     if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
       this.context.router.history.replace('/timelines/home');
       return;
     }
 
-    if (myAccount.get('locked')) {
-      fetchFollowRequests();
-    }
+    fetchFollowRequests();
   }
 
   render () {
@@ -134,7 +132,7 @@ class GettingStarted extends ImmutablePureComponent {
 
     height += 48*3;
 
-    if (myAccount.get('locked')) {
+    if (myAccount.get('locked') || unreadFollowRequests > 0) {
       navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
       height += 48;
     }
diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.js b/app/javascript/mastodon/features/notifications/components/filter_bar.js
index 3f3e6ab7d..2fd28d832 100644
--- a/app/javascript/mastodon/features/notifications/components/filter_bar.js
+++ b/app/javascript/mastodon/features/notifications/components/filter_bar.js
@@ -64,7 +64,7 @@ class FilterBar extends React.PureComponent {
           onClick={this.onClick('mention')}
           title={intl.formatMessage(tooltips.mentions)}
         >
-          <Icon id='at' fixedWidth />
+          <Icon id='reply-all' fixedWidth />
         </button>
         <button
           className={selectedFilter === 'favourite' ? 'active' : ''}
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 7e5de0613..d16a0f33a 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Column from '../../components/column';
 import ColumnHeader from '../../components/column_header';
-import { expandNotifications, scrollTopNotifications, loadPending } from '../../actions/notifications';
+import { expandNotifications, scrollTopNotifications, loadPending, mountNotifications, unmountNotifications } from '../../actions/notifications';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import NotificationContainer from './containers/notification_container';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -66,11 +66,16 @@ class Notifications extends React.PureComponent {
     trackScroll: true,
   };
 
+  componentWillMount() {
+    this.props.dispatch(mountNotifications());
+  }
+
   componentWillUnmount () {
     this.handleLoadOlder.cancel();
     this.handleScrollToTop.cancel();
     this.handleScroll.cancel();
     this.props.dispatch(scrollTopNotifications(false));
+    this.props.dispatch(unmountNotifications());
   }
 
   handleLoadGap = (maxId) => {
diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js
index 229f626b3..7edcf1968 100644
--- a/app/javascript/mastodon/features/reblogs/index.js
+++ b/app/javascript/mastodon/features/reblogs/index.js
@@ -27,7 +27,9 @@ class Reblogs extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchReblogs(this.props.params.statusId));
+    if (!this.props.accountIds) {
+      this.props.dispatch(fetchReblogs(this.props.params.statusId));
+    }
   }
 
   componentWillReceiveProps(nextProps) {
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 61e0c428a..333c295dc 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -1,4 +1,3 @@
-import React from 'react';
 import { connect } from 'react-redux';
 import DetailedStatus from '../components/detailed_status';
 import { makeGetStatus } from '../../../selectors';
@@ -15,7 +14,6 @@ import {
   pin,
   unpin,
 } from '../../../actions/interactions';
-import { blockAccount } from '../../../actions/accounts';
 import {
   muteStatus,
   unmuteStatus,
@@ -24,9 +22,10 @@ import {
   revealStatus,
 } from '../../../actions/statuses';
 import { initMuteModal } from '../../../actions/mutes';
+import { initBlockModal } from '../../../actions/blocks';
 import { initReport } from '../../../actions/reports';
 import { openModal } from '../../../actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import { boostModal, deleteModal } from '../../../initial_state';
 import { showAlertForError } from '../../../actions/alerts';
 
@@ -35,10 +34,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   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.' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   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?' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
 });
 
 const makeMapStateToProps = () => {
@@ -138,16 +135,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onBlock (status) {
     const account = status.get('account');
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.blockConfirm),
-      onConfirm: () => dispatch(blockAccount(account.get('id'))),
-      secondary: intl.formatMessage(messages.blockAndReport),
-      onSecondary: () => {
-        dispatch(blockAccount(account.get('id')));
-        dispatch(initReport(account, status));
-      },
-    }));
+    dispatch(initBlockModal(account));
   },
 
   onReport (status) {
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index f78a9489a..029057d40 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -23,7 +23,6 @@ import {
   mentionCompose,
   directCompose,
 } from '../../actions/compose';
-import { blockAccount } from '../../actions/accounts';
 import {
   muteStatus,
   unmuteStatus,
@@ -32,6 +31,7 @@ import {
   revealStatus,
 } from '../../actions/statuses';
 import { initMuteModal } from '../../actions/mutes';
+import { initBlockModal } from '../../actions/blocks';
 import { initReport } from '../../actions/reports';
 import { makeGetStatus } from '../../selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
@@ -39,7 +39,7 @@ import ColumnBackButton from '../../components/column_back_button';
 import ColumnHeader from '../../components/column_header';
 import StatusContainer from '../../containers/status_container';
 import { openModal } from '../../actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
 import { boostModal, deleteModal } from '../../initial_state';
@@ -52,13 +52,11 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   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.' },
-  blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
   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?' },
-  blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
 });
 
 const makeMapStateToProps = () => {
@@ -296,19 +294,9 @@ class Status extends ImmutablePureComponent {
   }
 
   handleBlockClick = (status) => {
-    const { dispatch, intl } = this.props;
+    const { dispatch } = this.props;
     const account = status.get('account');
-
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.blockConfirm),
-      onConfirm: () => dispatch(blockAccount(account.get('id'))),
-      secondary: intl.formatMessage(messages.blockAndReport),
-      onSecondary: () => {
-        dispatch(blockAccount(account.get('id')));
-        dispatch(initReport(account, status));
-      },
-    }));
+    dispatch(initBlockModal(account));
   }
 
   handleReport = (status) => {
diff --git a/app/javascript/mastodon/features/ui/components/block_modal.js b/app/javascript/mastodon/features/ui/components/block_modal.js
new file mode 100644
index 000000000..a07baeaa6
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/block_modal.js
@@ -0,0 +1,103 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { makeGetAccount } from '../../../selectors';
+import Button from '../../../components/button';
+import { closeModal } from '../../../actions/modal';
+import { blockAccount } from '../../../actions/accounts';
+import { initReport } from '../../../actions/reports';
+
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = state => ({
+    account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onConfirm(account) {
+      dispatch(blockAccount(account.get('id')));
+    },
+
+    onBlockAndReport(account) {
+      dispatch(blockAccount(account.get('id')));
+      dispatch(initReport(account));
+    },
+
+    onClose() {
+      dispatch(closeModal());
+    },
+  };
+};
+
+export default @connect(makeMapStateToProps, mapDispatchToProps)
+@injectIntl
+class BlockModal extends React.PureComponent {
+
+  static propTypes = {
+    account: PropTypes.object.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onBlockAndReport: PropTypes.func.isRequired,
+    onConfirm: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentDidMount() {
+    this.button.focus();
+  }
+
+  handleClick = () => {
+    this.props.onClose();
+    this.props.onConfirm(this.props.account);
+  }
+
+  handleSecondary = () => {
+    this.props.onClose();
+    this.props.onBlockAndReport(this.props.account);
+  }
+
+  handleCancel = () => {
+    this.props.onClose();
+  }
+
+  setRef = (c) => {
+    this.button = c;
+  }
+
+  render () {
+    const { account } = this.props;
+
+    return (
+      <div className='modal-root__modal block-modal'>
+        <div className='block-modal__container'>
+          <p>
+            <FormattedMessage
+              id='confirmations.block.message'
+              defaultMessage='Are you sure you want to block {name}?'
+              values={{ name: <strong>@{account.get('acct')}</strong> }}
+            />
+          </p>
+        </div>
+
+        <div className='block-modal__action-bar'>
+          <Button onClick={this.handleCancel} className='block-modal__cancel-button'>
+            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
+          </Button>
+          <Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
+            <FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
+          </Button>
+          <Button onClick={this.handleClick} ref={this.setRef}>
+            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index 7891d6690..1ab79a21d 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -173,7 +173,17 @@ class FocalPointModal extends ImmutablePureComponent {
         langPath: `${assetHost}/ocr/lang-data`,
       });
 
-      worker.recognize(media.get('url'))
+      let media_url = media.get('file');
+
+      if (window.URL && URL.createObjectURL) {
+        try {
+          media_url = URL.createObjectURL(media.get('file'));
+        } catch (error) {
+          console.error(error);
+        }
+      }
+
+      worker.recognize(media_url)
         .progress(({ progress }) => this.setState({ progress }))
         .finally(() => worker.terminate())
         .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 06f9e1bc4..58d3ba8db 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Base from '../../../components/modal_root';
+import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
+import Base from 'mastodon/components/modal_root';
 import BundleContainer from '../containers/bundle_container';
 import BundleModalError from './bundle_modal_error';
 import ModalLoading from './modal_loading';
@@ -12,6 +13,7 @@ import ConfirmationModal from './confirmation_modal';
 import FocalPointModal from './focal_point_modal';
 import {
   MuteModal,
+  BlockModal,
   ReportModal,
   EmbedModal,
   ListEditor,
@@ -24,6 +26,7 @@ const MODAL_COMPONENTS = {
   'BOOST': () => Promise.resolve({ default: BoostModal }),
   'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
   'MUTE': MuteModal,
+  'BLOCK': BlockModal,
   'REPORT': ReportModal,
   'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
   'EMBED': EmbedModal,
@@ -32,28 +35,6 @@ const MODAL_COMPONENTS = {
   'LIST_ADDER':ListAdder,
 };
 
-let cachedScrollbarWidth = null;
-
-export const getScrollbarWidth = () => {
-  if (cachedScrollbarWidth !== null) {
-    return cachedScrollbarWidth;
-  }
-
-  const outer = document.createElement('div');
-  outer.style.visibility = 'hidden';
-  outer.style.overflow = 'scroll';
-  document.body.appendChild(outer);
-
-  const inner = document.createElement('div');
-  outer.appendChild(inner);
-
-  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
-  cachedScrollbarWidth = scrollbarWidth;
-  outer.parentNode.removeChild(outer);
-
-  return scrollbarWidth;
-};
-
 export default class ModalRoot extends React.PureComponent {
 
   static propTypes = {
diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js
index ac356b42a..c364c5ba2 100644
--- a/app/javascript/mastodon/features/ui/components/mute_modal.js
+++ b/app/javascript/mastodon/features/ui/components/mute_modal.js
@@ -11,7 +11,6 @@ import { toggleHideNotifications } from '../../../actions/mutes';
 
 const mapStateToProps = state => {
   return {
-    isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
     account: state.getIn(['mutes', 'new', 'account']),
     notifications: state.getIn(['mutes', 'new', 'notifications']),
   };
@@ -38,7 +37,6 @@ export default @connect(mapStateToProps, mapDispatchToProps)
 class MuteModal extends React.PureComponent {
 
   static propTypes = {
-    isSubmitting: PropTypes.bool.isRequired,
     account: PropTypes.object.isRequired,
     notifications: PropTypes.bool.isRequired,
     onClose: PropTypes.func.isRequired,
@@ -81,11 +79,16 @@ class MuteModal extends React.PureComponent {
               values={{ name: <strong>@{account.get('acct')}</strong> }}
             />
           </p>
-          <div>
-            <label htmlFor='mute-modal__hide-notifications-checkbox'>
+          <p className='mute-modal__explanation'>
+            <FormattedMessage
+              id='confirmations.mute.explanation'
+              defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts follow you.'
+            />
+          </p>
+          <div className='setting-toggle'>
+            <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
+            <label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
               <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
-              {' '}
-              <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
             </label>
           </div>
         </div>
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 0084c1510..bb0fcb859 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -106,6 +106,10 @@ export function MuteModal () {
   return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
 }
 
+export function BlockModal () {
+  return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal');
+}
+
 export function ReportModal () {
   return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
 }
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index ce66a5d1c..8e2efe136 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -7,7 +7,7 @@
   "account.cancel_follow_request": "إلغاء طلب المتابَعة",
   "account.direct": "رسالة خاصة إلى @{name}",
   "account.domain_blocked": "النطاق مخفي",
-  "account.edit_profile": "تعديل الملف الشخصي",
+  "account.edit_profile": "تعديل الملف التعريفي",
   "account.endorse": "أوصِ به على صفحتك",
   "account.follow": "تابِع",
   "account.followers": "متابعون",
@@ -30,11 +30,11 @@
   "account.posts_with_replies": "التبويقات و الردود",
   "account.report": "ابلِغ عن @{name}",
   "account.requested": "في انتظار الموافقة. اضْغَطْ/ي لإلغاء طلب المتابعة",
-  "account.share": "مشاركة حساب @{name}",
+  "account.share": "شارك ملف تعريف @{name}",
   "account.show_reblogs": "اعرض ترقيات @{name}",
   "account.unblock": "إلغاء الحظر عن @{name}",
   "account.unblock_domain": "فك الخْفى عن {domain}",
-  "account.unendorse": "أزل ترويجه مِن الملف الشخصي",
+  "account.unendorse": "أزل ترويجه مِن الملف التعريفي",
   "account.unfollow": "إلغاء المتابعة",
   "account.unmute": "إلغاء الكتم عن @{name}",
   "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
@@ -53,7 +53,7 @@
   "column.blocks": "الحسابات المحجوبة",
   "column.community": "الخيط العام المحلي",
   "column.direct": "الرسائل المباشرة",
-  "column.directory": "Browse profiles",
+  "column.directory": "استعرض الملفات التعريفية",
   "column.domain_blocks": "النطاقات المخفية",
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
@@ -63,7 +63,7 @@
   "column.notifications": "الإخطارات",
   "column.pins": "التبويقات المثبتة",
   "column.public": "الخيط العام الموحد",
-  "column.status": "Toot",
+  "column.status": "تبويق",
   "column_back_button.label": "العودة",
   "column_header.hide_settings": "إخفاء الإعدادات",
   "column_header.moveLeft_settings": "نقل القائمة إلى اليسار",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "الرد في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد كتابتها. متأكد من أنك تريد المواصلة؟",
   "confirmations.unfollow.confirm": "إلغاء المتابعة",
   "confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟",
+  "conversation.delete": "احذف المحادثة",
+  "conversation.mark_as_read": "اعتبرها كمقروءة",
+  "conversation.open": "اعرض المحادثة",
+  "conversation.with": "بـ {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "الوافدون الجُدد",
@@ -132,7 +136,7 @@
   "emoji_button.symbols": "رموز",
   "emoji_button.travel": "الأماكن والسفر",
   "empty_column.account_timeline": "ليس هناك تبويقات!",
-  "empty_column.account_unavailable": "الملف الشخصي غير متوفر",
+  "empty_column.account_unavailable": "الملف التعريفي غير متوفر",
   "empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.",
   "empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!",
   "empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.",
@@ -151,7 +155,7 @@
   "follow_request.authorize": "ترخيص",
   "follow_request.reject": "رفض",
   "getting_started.developers": "المُطوِّرون",
-  "getting_started.directory": "دليل المستخدِمين والمستخدِمات",
+  "getting_started.directory": "دليل الصفحات التعريفية",
   "getting_started.documentation": "الدليل",
   "getting_started.heading": "استعدّ للبدء",
   "getting_started.invite": "دعوة أشخاص",
@@ -190,7 +194,7 @@
   "introduction.interactions.reply.text": "يمكنكم الرد على تبويقاتكم و تبويقات الآخرين على شكل سلسلة محادثة.",
   "introduction.welcome.action": "هيا بنا!",
   "introduction.welcome.headline": "الخطوات الأولى",
-  "introduction.welcome.text": "مرحبا بكم على الفديفرس! بعد لحظات قليلة ، سيكون بمقدوركم بث رسائل والتحدث إلى أصدقائكم عبر تشكيلة واسعة من الخوادم المختلفة. هذا الخادم ، {domain} ، يستضيف ملفكم الشخصي ، لذا يجب تذكر اسمه جيدا.",
+  "introduction.welcome.text": "مرحبا بكم على الفديفرس! بعد لحظات قليلة ، سيكون بمقدوركم بث رسائل والتحدث إلى أصدقائكم عبر تشكيلة واسعة من الخوادم المختلفة. هذا الخادم ، {domain} ، يستضيف صفحتكم التعريفية ، لذا يجب تذكر اسمه جيدا.",
   "keyboard_shortcuts.back": "للعودة",
   "keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين",
   "keyboard_shortcuts.boost": "للترقية",
@@ -210,10 +214,10 @@
   "keyboard_shortcuts.local": "لفتح الخيط العام المحلي",
   "keyboard_shortcuts.mention": "لذِكر الناشر",
   "keyboard_shortcuts.muted": "لفتح قائمة المستخدِمين المكتومين",
-  "keyboard_shortcuts.my_profile": "لفتح ملفك الشخصي",
+  "keyboard_shortcuts.my_profile": "لفتح ملفك التعريفي",
   "keyboard_shortcuts.notifications": "لفتح عمود الإشعارات",
   "keyboard_shortcuts.pinned": "لفتح قائمة التبويقات المدبسة",
-  "keyboard_shortcuts.profile": "لفتح رابط الناشر",
+  "keyboard_shortcuts.profile": "لفتح الملف التعريفي للناشر",
   "keyboard_shortcuts.reply": "للردّ",
   "keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة",
   "keyboard_shortcuts.search": "للتركيز على البحث",
@@ -249,7 +253,7 @@
   "navigation_bar.direct": "الرسائل المباشِرة",
   "navigation_bar.discover": "اكتشف",
   "navigation_bar.domain_blocks": "النطاقات المخفية",
-  "navigation_bar.edit_profile": "تعديل الملف الشخصي",
+  "navigation_bar.edit_profile": "عدّل الملف التعريفي",
   "navigation_bar.favourites": "المفضلة",
   "navigation_bar.filters": "الكلمات المكتومة",
   "navigation_bar.follow_requests": "طلبات المتابعة",
@@ -295,6 +299,7 @@
   "poll.refresh": "تحديث",
   "poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}",
   "poll.vote": "صَوّت",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "إضافة استطلاع للرأي",
   "poll_button.remove_poll": "إزالة استطلاع الرأي",
   "privacy.change": "اضبط خصوصية المنشور",
@@ -351,7 +356,7 @@
   "status.mute": "أكتم @{name}",
   "status.mute_conversation": "كتم المحادثة",
   "status.open": "وسع هذه المشاركة",
-  "status.pin": "تدبيس على الملف الشخصي",
+  "status.pin": "دبّسه على الصفحة التعريفية",
   "status.pinned": "تبويق مثبَّت",
   "status.read_more": "اقرأ المزيد",
   "status.reblog": "رَقِّي",
@@ -371,7 +376,7 @@
   "status.show_thread": "الكشف عن المحادثة",
   "status.uncached_media_warning": "غير متوفر",
   "status.unmute_conversation": "فك الكتم عن المحادثة",
-  "status.unpin": "فك التدبيس من الملف الشخصي",
+  "status.unpin": "فك التدبيس من الصفحة التعريفية",
   "suggestions.dismiss": "إلغاء الاقتراح",
   "suggestions.header": "يمكن أن يهمك…",
   "tabs_bar.federated_timeline": "الموحَّد",
@@ -400,7 +405,7 @@
   "upload_modal.detect_text": "Detect text from picture",
   "upload_modal.edit_media": "تعديل الوسائط",
   "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
-  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_modal.preview_label": "معاينة ({ratio})",
   "upload_progress.label": "يرفع...",
   "video.close": "إغلاق الفيديو",
   "video.exit_fullscreen": "الخروج من وضع الشاشة المليئة",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 2ef693fcb..76721749c 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 309f04513..b230d1863 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -1,12 +1,12 @@
 {
-  "account.add_or_remove_from_list": "Add or Remove from lists",
-  "account.badges.bot": "Bot",
+  "account.add_or_remove_from_list": "Добави или премахни от списъците",
+  "account.badges.bot": "бот",
   "account.block": "Блокирай",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.blocked": "Blocked",
+  "account.block_domain": "скрий всичко от (домейн)",
+  "account.blocked": "Блокирани",
   "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "Скрит домейн",
   "account.edit_profile": "Редактирай профила си",
   "account.endorse": "Feature on profile",
   "account.follow": "Последвай",
@@ -45,7 +45,7 @@
   "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.retry": "Опитай отново",
   "bundle_column_error.title": "Network error",
   "bundle_modal_error.close": "Close",
   "bundle_modal_error.message": "Something went wrong while loading this component.",
@@ -58,7 +58,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Начало",
-  "column.lists": "Lists",
+  "column.lists": "Списъци",
   "column.mutes": "Muted users",
   "column.notifications": "Известия",
   "column.pins": "Pinned toot",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json
index e4984a118..8109d97eb 100644
--- a/app/javascript/mastodon/locales/bn.json
+++ b/app/javascript/mastodon/locales/bn.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?",
   "confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে",
   "confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "বদলেছে কিনা দেখতে",
   "poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}",
   "poll.vote": "ভোট",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "একটা নির্বাচন যোগ করতে",
   "poll_button.remove_poll": "নির্বাচন বাদ দিতে",
   "privacy.change": "লেখার গোপনীয়তা অবস্থা ঠিক করতে",
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index 2de037f16..fbe8020bd 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -1,60 +1,60 @@
 {
-  "account.add_or_remove_from_list": "Add or Remove from lists",
-  "account.badges.bot": "Bot",
-  "account.block": "Block @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.blocked": "Blocked",
-  "account.cancel_follow_request": "Cancel follow request",
-  "account.direct": "Direct message @{name}",
-  "account.domain_blocked": "Domain hidden",
-  "account.edit_profile": "Edit profile",
-  "account.endorse": "Feature on profile",
-  "account.follow": "Follow",
-  "account.followers": "Followers",
-  "account.followers.empty": "No one follows this user yet.",
-  "account.follows": "Follows",
+  "account.add_or_remove_from_list": "Ouzhpenn pe lemel ag ar listennadoù",
+  "account.badges.bot": "Robot",
+  "account.block": "Stankañ @{name}",
+  "account.block_domain": "Kuzh kement tra a {domain}",
+  "account.blocked": "Stanket",
+  "account.cancel_follow_request": "Nullañ ar pedad heuliañ",
+  "account.direct": "Kas ur c'hemennad da @{name}",
+  "account.domain_blocked": "Domani kuzhet",
+  "account.edit_profile": "Aozañ ar profil",
+  "account.endorse": "Lakaat war-wel war ar profil",
+  "account.follow": "Heuliañ",
+  "account.followers": "Heilour·ezed·ion",
+  "account.followers.empty": "Den na heul an implijour-mañ c'hoazh.",
+  "account.follows": "Koumanantoù",
   "account.follows.empty": "This user doesn't follow anyone yet.",
-  "account.follows_you": "Follows you",
-  "account.hide_reblogs": "Hide boosts from @{name}",
-  "account.last_status": "Last active",
+  "account.follows_you": "Ho heul",
+  "account.hide_reblogs": "Kuzh toudoù skignet gant @{name}",
+  "account.last_status": "Oberiantiz zivezhañ",
   "account.link_verified_on": "Ownership of this link was checked on {date}",
   "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
   "account.media": "Media",
-  "account.mention": "Mention @{name}",
-  "account.moved_to": "{name} has moved to:",
-  "account.mute": "Mute @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
-  "account.muted": "Muted",
-  "account.never_active": "Never",
-  "account.posts": "Toots",
-  "account.posts_with_replies": "Toots and replies",
-  "account.report": "Report @{name}",
+  "account.mention": "Menegiñ @{name}",
+  "account.moved_to": "Dilojet en·he deus {name} da:",
+  "account.mute": "Kuzhat @{name}",
+  "account.mute_notifications": "Kuzh kemennoù a @{name}",
+  "account.muted": "Kuzhet",
+  "account.never_active": "Birviken",
+  "account.posts": "Toudoù",
+  "account.posts_with_replies": "Toudoù ha respontoù",
+  "account.report": "Disklêriañ @{name}",
   "account.requested": "Awaiting approval",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
-  "account.unblock": "Unblock @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.share": "Skignañ profil @{name}",
+  "account.show_reblogs": "Diskouez toudoù a @{name}",
+  "account.unblock": "Distankañ @{name}",
+  "account.unblock_domain": "Diguzh {domain}",
   "account.unendorse": "Don't feature on profile",
-  "account.unfollow": "Unfollow",
-  "account.unmute": "Unmute @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unfollow": "Diheuliañ",
+  "account.unmute": "Diguzhat @{name}",
+  "account.unmute_notifications": "Diguzhat kemennoù a @{name}",
   "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
   "alert.rate_limited.title": "Rate limited",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
+  "alert.unexpected.title": "C'hem !",
+  "autosuggest_hashtag.per_week": "{count} bep sizhun",
   "boost_modal.combo": "You can press {combo} to skip this next time",
   "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
+  "bundle_column_error.retry": "Klask endro",
+  "bundle_column_error.title": "Fazi rouedad",
+  "bundle_modal_error.close": "Serriñ",
   "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
-  "column.blocks": "Blocked users",
-  "column.community": "Local timeline",
-  "column.direct": "Direct messages",
-  "column.directory": "Browse profiles",
-  "column.domain_blocks": "Hidden domains",
+  "bundle_modal_error.retry": "Klask endro",
+  "column.blocks": "Implijour·ezed·ion stanket",
+  "column.community": "Red-amzer lec'hel",
+  "column.direct": "Kemennadoù prevez",
+  "column.directory": "Mont a-dreuz ar profiloù",
+  "column.domain_blocks": "Domani kuzhet",
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 77f84ac7d..5b05c09d5 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Responen ara es sobreescriurà el missatge que estàs editant. Estàs segur que vols continuar?",
   "confirmations.unfollow.confirm": "Deixa de seguir",
   "confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Actualitza",
   "poll.total_votes": "{count, plural, one {# vot} other {# vots}}",
   "poll.vote": "Vota",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Afegeix una enquesta",
   "poll_button.remove_poll": "Elimina l'enquesta",
   "privacy.change": "Ajusta l'estat de privacitat",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index d95f32b18..7ebe08cf5 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -63,7 +63,7 @@
   "column.notifications": "Nutificazione",
   "column.pins": "Statuti puntarulati",
   "column.public": "Linea pubblica glubale",
-  "column.status": "Toot",
+  "column.status": "Statutu",
   "column_back_button.label": "Ritornu",
   "column_header.hide_settings": "Piattà i parametri",
   "column_header.moveLeft_settings": "Spiazzà à manca",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Risponde avà sguasserà u missaghju chì scrivite. Site sicuru·a chì vulete cuntinuà?",
   "confirmations.unfollow.confirm": "Disabbunassi",
   "confirmations.unfollow.message": "Site sicuru·a ch'ùn vulete più siguità @{name}?",
+  "conversation.delete": "Sguassà a cunversazione",
+  "conversation.mark_as_read": "Marcà cum'è lettu",
+  "conversation.open": "Vede a cunversazione",
+  "conversation.with": "Cù {names}",
   "directory.federated": "Da u fediverse cunisciutu",
   "directory.local": "Solu da {domain}",
   "directory.new_arrivals": "Ultimi arrivi",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Bàsichi",
   "home.column_settings.show_reblogs": "Vede e spartere",
   "home.column_settings.show_replies": "Vede e risposte",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Attualizà in tempu reale",
   "intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}",
   "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Preferenze",
   "navigation_bar.public_timeline": "Linea pubblica glubale",
   "navigation_bar.security": "Sicurità",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "è {count, plural, one {# altru} other {# altri}}",
   "notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti",
   "notification.follow": "{name} v'hà seguitatu",
   "notification.mention": "{name} v'hà mintuvatu",
@@ -295,6 +299,7 @@
   "poll.refresh": "Attualizà",
   "poll.total_votes": "{count, plural, one {# votu} other {# voti}}",
   "poll.vote": "Vutà",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Aghjunghje",
   "poll_button.remove_poll": "Toglie u scandagliu",
   "privacy.change": "Mudificà a cunfidenzialità di u statutu",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 8acf27cb3..94a659435 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Odpovězením nyní přepíšete zprávu, kterou aktuálně píšete. Jste si jistý/á, že chcete pokračovat?",
   "confirmations.unfollow.confirm": "Přestat sledovat",
   "confirmations.unfollow.message": "jste si jistý/á, že chcete přestat sledovat uživatele {name}?",
+  "conversation.delete": "Smazat konverzaci",
+  "conversation.mark_as_read": "Označit jako přečtenou",
+  "conversation.open": "Zobrazit konverzaci",
+  "conversation.with": "S {names}",
   "directory.federated": "Ze známého fedivesmíru",
   "directory.local": "Pouze z {domain}",
   "directory.new_arrivals": "Nově příchozí",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Základní",
   "home.column_settings.show_reblogs": "Zobrazit boosty",
   "home.column_settings.show_replies": "Zobrazit odpovědi",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Aktualizovat v reálném čase",
   "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}",
   "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Předvolby",
   "navigation_bar.public_timeline": "Federovaná časová osa",
   "navigation_bar.security": "Zabezpečení",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}}",
   "notification.favourite": "{name} si oblíbil/a váš toot",
   "notification.follow": "{name} vás začal/a sledovat",
   "notification.mention": "{name} vás zmínil/a",
@@ -295,6 +299,7 @@
   "poll.refresh": "Obnovit",
   "poll.total_votes": "{count, plural, one {# hlas} few {# hlasy} many {# hlasu} other {# hlasů}}",
   "poll.vote": "Hlasovat",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Přidat anketu",
   "poll_button.remove_poll": "Odstranit anketu",
   "privacy.change": "Změnit soukromí tootu",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index cdf2656d7..c7f679b22 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -4,7 +4,7 @@
   "account.block": "Blocio @{name}",
   "account.block_domain": "Cuddio popeth rhag {domain}",
   "account.blocked": "Blociwyd",
-  "account.cancel_follow_request": "Cancel follow request",
+  "account.cancel_follow_request": "Canslo cais dilyn",
   "account.direct": "Neges breifat @{name}",
   "account.domain_blocked": "Parth wedi ei guddio",
   "account.edit_profile": "Golygu proffil",
@@ -16,7 +16,7 @@
   "account.follows.empty": "Nid yw'r defnyddiwr hwn yn dilyn unrhyw un eto.",
   "account.follows_you": "Yn eich dilyn chi",
   "account.hide_reblogs": "Cuddio bwstiau o @{name}",
-  "account.last_status": "Last active",
+  "account.last_status": "Gweithredol olaf",
   "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}",
   "account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i gloi. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.",
   "account.media": "Cyfryngau",
@@ -25,7 +25,7 @@
   "account.mute": "Tawelu @{name}",
   "account.mute_notifications": "Cuddio hysbysiadau o @{name}",
   "account.muted": "Distewyd",
-  "account.never_active": "Never",
+  "account.never_active": "Byth",
   "account.posts": "Tŵtiau",
   "account.posts_with_replies": "Tŵtiau ac atebion",
   "account.report": "Adrodd @{name}",
@@ -38,11 +38,11 @@
   "account.unfollow": "Dad-ddilyn",
   "account.unmute": "Dad-dawelu @{name}",
   "account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "Ceisiwch eto ar ôl {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Cyfradd gyfyngedig",
   "alert.unexpected.message": "Digwyddodd gwall annisgwyl.",
   "alert.unexpected.title": "Wps!",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "autosuggest_hashtag.per_week": "{count} yr wythnos",
   "boost_modal.combo": "Mae modd gwasgu {combo} er mwyn sgipio hyn tro nesa",
   "bundle_column_error.body": "Aeth rhywbeth o'i le tra'n llwytho'r elfen hon.",
   "bundle_column_error.retry": "Ceisiwch eto",
@@ -53,7 +53,7 @@
   "column.blocks": "Defnyddwyr a flociwyd",
   "column.community": "Ffrwd lleol",
   "column.direct": "Negeseuon preifat",
-  "column.directory": "Browse profiles",
+  "column.directory": "Pori proffiliau",
   "column.domain_blocks": "Parthau cuddiedig",
   "column.favourites": "Ffefrynnau",
   "column.follow_requests": "Ceisiadau dilyn",
@@ -63,7 +63,7 @@
   "column.notifications": "Hysbysiadau",
   "column.pins": "Tŵtiau wedi eu pinio",
   "column.public": "Ffrwd y ffederasiwn",
-  "column.status": "Toot",
+  "column.status": "Tŵt",
   "column_back_button.label": "Nôl",
   "column_header.hide_settings": "Cuddio dewisiadau",
   "column_header.moveLeft_settings": "Symud y golofn i'r chwith",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "Ydych chi'n sicr eich bod eisiau dileu y rhestr hwn am byth?",
   "confirmations.domain_block.confirm": "Cuddio parth cyfan",
   "confirmations.domain_block.message": "A ydych yn hollol, hollol sicr eich bod am flocio y {domain} cyfan? Yn y nifer helaeth o achosion mae blocio neu tawelu ambell gyfrif yn ddigonol ac yn well. Ni fyddwch yn gweld cynnwys o'r parth hwnnw mewn unrhyw ffrydiau cyhoeddus na chwaith yn eich hysbysiadau. Bydd hyn yn cael gwared o'ch dilynwyr o'r parth hwnnw.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Allgofnodi",
+  "confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?",
   "confirmations.mute.confirm": "Tawelu",
   "confirmations.mute.message": "Ydych chi'n sicr eich bod am ddistewi {name}?",
   "confirmations.redraft.confirm": "Dileu & ailddrafftio",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Bydd ateb nawr yn cymryd lle y neges yr ydych yn cyfansoddi ar hyn o bryd. Ydych chi'n sicr yr ydych am barhau?",
   "confirmations.unfollow.confirm": "Dad-ddilynwch",
   "confirmations.unfollow.message": "Ydych chi'n sicr eich bod am ddad-ddilyn {name}?",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "Dileu sgwrs",
+  "conversation.mark_as_read": "Nodi fel wedi'i ddarllen",
+  "conversation.open": "Gweld sgwrs",
+  "conversation.with": "Gyda {names}",
+  "directory.federated": "O ffedysawd hysbys",
+  "directory.local": "O {domain} yn unig",
+  "directory.new_arrivals": "Newydd-ddyfodiaid",
+  "directory.recently_active": "Yn weithredol yn ddiweddar",
   "embed.instructions": "Mewnblannwch y tŵt hwn ar eich gwefan drwy gopïo'r côd isod.",
   "embed.preview": "Dyma sut olwg fydd arno:",
   "emoji_button.activity": "Gweithgarwch",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Syml",
   "home.column_settings.show_reblogs": "Dangos bŵstiau",
   "home.column_settings.show_replies": "Dangos ymatebion",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Diweddariad mewn amser real",
   "intervals.full.days": "{number, plural, one {# ddydd} other {# o ddyddiau}}",
   "intervals.full.hours": "{number, plural, one {# awr} other {# o oriau}}",
   "intervals.full.minutes": "{number, plural, one {# funud} other {# o funudau}}",
@@ -236,7 +240,7 @@
   "lists.new.title_placeholder": "Teitl rhestr newydd",
   "lists.search": "Chwilio ymysg pobl yr ydych yn ei ddilyn",
   "lists.subheading": "Eich rhestrau",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# eitem newydd} other {# eitemau newydd}}",
   "loading_indicator.label": "Llwytho...",
   "media_gallery.toggle_visible": "Toglo gwelededd",
   "missing_indicator.label": "Heb ei ganfod",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Dewisiadau",
   "navigation_bar.public_timeline": "Ffrwd y ffederasiwn",
   "navigation_bar.security": "Diogelwch",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "a {count, plural, one {# arall} other {# eraill}}",
   "notification.favourite": "hoffodd {name} eich tŵt",
   "notification.follow": "dilynodd {name} chi",
   "notification.mention": "Soniodd {name} amdanoch chi",
@@ -295,6 +299,7 @@
   "poll.refresh": "Adnewyddu",
   "poll.total_votes": "{count, plural, one {# bleidlais} other {# o bleidleisiau}}",
   "poll.vote": "Pleidleisio",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Ychwanegu pleidlais",
   "poll_button.remove_poll": "Tynnu pleidlais",
   "privacy.change": "Addasu preifatrwdd y tŵt",
@@ -330,7 +335,7 @@
   "search_results.accounts": "Pobl",
   "search_results.hashtags": "Hanshnodau",
   "search_results.statuses": "Tŵtiau",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Nid yw chwilio Tŵtiau yn ôl eu cynnwys wedi'i alluogi ar y gweinydd Mastodon hwn.",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "status.admin_account": "Agor rhyngwyneb goruwchwylio ar gyfer @{name}",
   "status.admin_status": "Agor y tŵt yn y rhyngwyneb goruwchwylio",
@@ -369,7 +374,7 @@
   "status.show_more": "Dangos mwy",
   "status.show_more_all": "Dangos mwy i bawb",
   "status.show_thread": "Dangos edefyn",
-  "status.uncached_media_warning": "Not available",
+  "status.uncached_media_warning": "Dim ar gael",
   "status.unmute_conversation": "Dad-dawelu sgwrs",
   "status.unpin": "Dadbinio o'r proffil",
   "suggestions.dismiss": "Diswyddo",
@@ -385,22 +390,22 @@
   "time_remaining.moments": "Munudau ar ôl",
   "time_remaining.seconds": "{number, plural, one {# eiliad} other {# o eiliadau}} ar ôl",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad",
-  "trends.trending_now": "Trending now",
+  "trends.trending_now": "Yn tueddu nawr",
   "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.",
   "upload_area.title": "Llusgwch & gollwing i uwchlwytho",
   "upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Wedi mynd heibio'r uchafswm terfyn uwchlwytho.",
   "upload_error.poll": "Nid oes modd uwchlwytho ffeiliau â phleidleisiau.",
   "upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg",
-  "upload_form.edit": "Edit",
+  "upload_form.edit": "Golygu",
   "upload_form.undo": "Dileu",
-  "upload_modal.analyzing_picture": "Analyzing picture…",
-  "upload_modal.apply": "Apply",
-  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
-  "upload_modal.detect_text": "Detect text from picture",
-  "upload_modal.edit_media": "Edit media",
-  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
-  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_modal.analyzing_picture": "Dadansoddi llun…",
+  "upload_modal.apply": "Gweithredu",
+  "upload_modal.description_placeholder": "Mae ei phen bach llawn jocs, 'run peth a fy nghot golff, rhai dyddiau",
+  "upload_modal.detect_text": "Canfod testun o'r llun",
+  "upload_modal.edit_media": "Golygu cyfryngau",
+  "upload_modal.hint": "Cliciwch neu llusgwch y cylch ar y rhagolwg i ddewis y canolbwynt a fydd bob amser i'w weld ar bob mân-lunau.",
+  "upload_modal.preview_label": "Rhagolwg ({ratio})",
   "upload_progress.label": "Uwchlwytho...",
   "video.close": "Cau fideo",
   "video.exit_fullscreen": "Gadael sgrîn llawn",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 14b0f7563..852b6d157 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Hvis du svarer nu vil du overskrive den besked du er ved at skrive. Er du sikker på, du vil fortsætte?",
   "confirmations.unfollow.confirm": "Følg ikke længere",
   "confirmations.unfollow.message": "Er du sikker på, du ikke længere vil følge {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "Fra kendt fedivers",
   "directory.local": "Kun fra {domain}",
   "directory.new_arrivals": "Nye ankomster",
@@ -295,6 +299,7 @@
   "poll.refresh": "Opdatér",
   "poll.total_votes": "{count, plural, one {# stemme} other {# stemmer}}",
   "poll.vote": "Stem",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Tilføj en afstemning",
   "poll_button.remove_poll": "Fjern afstemning",
   "privacy.change": "Skift status visningsindstillinger",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 845bc5156..08b75c9ec 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -63,7 +63,7 @@
   "column.notifications": "Mitteilungen",
   "column.pins": "Angeheftete Beiträge",
   "column.public": "Föderierte Zeitleiste",
-  "column.status": "Toot",
+  "column.status": "Beitrag",
   "column_back_button.label": "Zurück",
   "column_header.hide_settings": "Einstellungen verbergen",
   "column_header.moveLeft_settings": "Spalte nach links verschieben",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Wenn du jetzt antwortest wird es die gesamte Nachricht verwerfen, die du gerade schreibst. Möchtest du wirklich fortfahren?",
   "confirmations.unfollow.confirm": "Entfolgen",
   "confirmations.unfollow.message": "Bist du dir sicher, dass du {name} entfolgen möchtest?",
+  "conversation.delete": "Unterhaltung löschen",
+  "conversation.mark_as_read": "Als gelesen markieren",
+  "conversation.open": "Unterhaltung anzeigen",
+  "conversation.with": "Mit {names}",
   "directory.federated": "Aus dem Fediverse",
   "directory.local": "Nur von {domain}",
   "directory.new_arrivals": "Neue Benutzer",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Einfach",
   "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
   "home.column_settings.show_replies": "Antworten anzeigen",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "In Echtzeit aktualisieren",
   "intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}",
   "intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}",
   "intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Einstellungen",
   "navigation_bar.public_timeline": "Föderierte Zeitleiste",
   "navigation_bar.security": "Sicherheit",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "und {count, plural, one {# andere Person} other {# andere Personen}}",
   "notification.favourite": "{name} hat deinen Beitrag favorisiert",
   "notification.follow": "{name} folgt dir",
   "notification.mention": "{name} hat dich erwähnt",
@@ -295,6 +299,7 @@
   "poll.refresh": "Aktualisieren",
   "poll.total_votes": "{count, plural, one {# Stimme} other {# Stimmen}}",
   "poll.vote": "Abstimmen",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Eine Umfrage erstellen",
   "poll_button.remove_poll": "Umfrage entfernen",
   "privacy.change": "Sichtbarkeit des Beitrags anpassen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 0d682c983..efefafe55 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -234,6 +234,11 @@
         "id": "poll.closed"
       },
       {
+        "defaultMessage": "You voted for this answer",
+        "description": "Tooltip of the \"voted\" checkmark in polls",
+        "id": "poll.voted"
+      },
+      {
         "defaultMessage": "Vote",
         "id": "poll.vote"
       },
@@ -1267,6 +1272,56 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "More",
+        "id": "status.more"
+      },
+      {
+        "defaultMessage": "View conversation",
+        "id": "conversation.open"
+      },
+      {
+        "defaultMessage": "Reply",
+        "id": "status.reply"
+      },
+      {
+        "defaultMessage": "Mark as read",
+        "id": "conversation.mark_as_read"
+      },
+      {
+        "defaultMessage": "Delete conversation",
+        "id": "conversation.delete"
+      },
+      {
+        "defaultMessage": "Mute conversation",
+        "id": "status.mute_conversation"
+      },
+      {
+        "defaultMessage": "Unmute conversation",
+        "id": "status.unmute_conversation"
+      },
+      {
+        "defaultMessage": "With {names}",
+        "id": "conversation.with"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/direct_timeline/components/conversation.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Reply",
+        "id": "confirmations.reply.confirm"
+      },
+      {
+        "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
+        "id": "confirmations.reply.message"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/direct_timeline/containers/conversation_container.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Direct messages",
         "id": "column.direct"
       },
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index bdd1da36c..9425c3f12 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -1,7 +1,7 @@
 {
   "account.add_or_remove_from_list": "Προσθήκη ή Αφαίρεση από λίστες",
   "account.badges.bot": "Μποτ",
-  "account.block": "Αποκλισμός @{name}",
+  "account.block": "Αποκλεισμός @{name}",
   "account.block_domain": "Απόκρυψε τα πάντα από το {domain}",
   "account.blocked": "Αποκλεισμένος/η",
   "account.cancel_follow_request": "Ακύρωση αιτήματος παρακολούθησης",
@@ -63,7 +63,7 @@
   "column.notifications": "Ειδοποιήσεις",
   "column.pins": "Καρφιτσωμένα τουτ",
   "column.public": "Ομοσπονδιακή ροή",
-  "column.status": "Toot",
+  "column.status": "Τουτ",
   "column_back_button.label": "Πίσω",
   "column_header.hide_settings": "Απόκρυψη ρυθμίσεων",
   "column_header.moveLeft_settings": "Μεταφορά κολώνας αριστερά",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Απαντώντας τώρα θα αντικαταστήσεις το κείμενο που ήδη γράφεις. Σίγουρα θέλεις να συνεχίσεις;",
   "confirmations.unfollow.confirm": "Διακοπή παρακολούθησης",
   "confirmations.unfollow.message": "Σίγουρα θες να πάψεις να ακολουθείς τον/την {name};",
+  "conversation.delete": "Διαγραφή συζήτησης",
+  "conversation.mark_as_read": "Σήμανση ως αναγνωσμένο",
+  "conversation.open": "Προβολή συνομιλίας",
+  "conversation.with": "Με {names}",
   "directory.federated": "Από το γνωστό fediverse",
   "directory.local": "Μόνο από {domain}",
   "directory.new_arrivals": "Νέες αφίξεις",
@@ -127,7 +131,7 @@
   "emoji_button.objects": "Αντικείμενα",
   "emoji_button.people": "Άνθρωποι",
   "emoji_button.recent": "Δημοφιλή",
-  "emoji_button.search": "Αναζήτηση…",
+  "emoji_button.search": "Αναζήτηση...",
   "emoji_button.search_results": "Αποτελέσματα αναζήτησης",
   "emoji_button.symbols": "Σύμβολα",
   "emoji_button.travel": "Ταξίδια & Τοποθεσίες",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Βασικές ρυθμίσεις",
   "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
   "home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Ενημέρωση σε πραγματικό χρόνο",
   "intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}",
   "intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}",
   "intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Προτιμήσεις",
   "navigation_bar.public_timeline": "Ομοσπονδιακή ροή",
   "navigation_bar.security": "Ασφάλεια",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "και {count, plural, one {# άλλη} other {# άλλες}}",
   "notification.favourite": "Ο/Η {name} σημείωσε ως αγαπημένη την κατάστασή σου",
   "notification.follow": "Ο/Η {name} σε ακολούθησε",
   "notification.mention": "Ο/Η {name} σε ανέφερε",
@@ -295,6 +299,7 @@
   "poll.refresh": "Ανανέωση",
   "poll.total_votes": "{count, plural, one {# ψήφος} other {# ψήφοι}}",
   "poll.vote": "Ψήφισε",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Προσθήκη δημοσκόπησης",
   "poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
   "privacy.change": "Προσαρμογή ιδιωτικότητας δημοσίευσης",
@@ -388,7 +393,7 @@
   "trends.trending_now": "Δημοφιλή τώρα",
   "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.",
   "upload_area.title": "Drag & drop για να ανεβάσεις",
-  "upload_button.label": "Πρόσθεσε πολυμέσα (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_button.label": "Πρόσθεσε πολυμέσα ({formats})",
   "upload_error.limit": "Υπέρβαση ορίου μεγέθους ανεβασμένων αρχείων.",
   "upload_error.poll": "Στις δημοσκοπήσεις δεν επιτρέπεται η μεταφόρτωση αρχείου.",
   "upload_form.description": "Περιέγραψε για όσους & όσες έχουν προβλήματα όρασης",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 15c579d51..27121d121 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -115,6 +115,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -300,6 +304,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 31750050e..bf08a08fb 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -4,7 +4,7 @@
   "account.block": "Bloki @{name}",
   "account.block_domain": "Kaŝi ĉion de {domain}",
   "account.blocked": "Blokita",
-  "account.cancel_follow_request": "Nuligi peto de sekvado",
+  "account.cancel_follow_request": "Nuligi peton de sekvado",
   "account.direct": "Rekte mesaĝi @{name}",
   "account.domain_blocked": "Domajno kaŝita",
   "account.edit_profile": "Redakti profilon",
@@ -16,14 +16,14 @@
   "account.follows.empty": "Tiu uzanto ankoraŭ ne sekvas iun.",
   "account.follows_you": "Sekvas vin",
   "account.hide_reblogs": "Kaŝi diskonigojn de @{name}",
-  "account.last_status": "Lasta aktiva",
+  "account.last_status": "Laste aktiva",
   "account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}",
   "account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.",
   "account.media": "Aŭdovidaĵoj",
   "account.mention": "Mencii @{name}",
   "account.moved_to": "{name} moviĝis al:",
   "account.mute": "Silentigi @{name}",
-  "account.mute_notifications": "Silentigi sciigojn el @{name}",
+  "account.mute_notifications": "Silentigi sciigojn de @{name}",
   "account.muted": "Silentigita",
   "account.never_active": "Neniam",
   "account.posts": "Mesaĝoj",
@@ -38,8 +38,8 @@
   "account.unfollow": "Ne plu sekvi",
   "account.unmute": "Malsilentigi @{name}",
   "account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
-  "alert.rate_limited.message": "Bonvolu reprovi poste {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Mesaĝkvante limigita",
   "alert.unexpected.message": "Neatendita eraro okazis.",
   "alert.unexpected.title": "Ups!",
   "autosuggest_hashtag.per_week": "{count} semajne",
@@ -53,7 +53,7 @@
   "column.blocks": "Blokitaj uzantoj",
   "column.community": "Loka tempolinio",
   "column.direct": "Rektaj mesaĝoj",
-  "column.directory": "Browse profiles",
+  "column.directory": "Trarigardi profilojn",
   "column.domain_blocks": "Kaŝitaj domajnoj",
   "column.favourites": "Stelumoj",
   "column.follow_requests": "Petoj de sekvado",
@@ -63,7 +63,7 @@
   "column.notifications": "Sciigoj",
   "column.pins": "Alpinglitaj mesaĝoj",
   "column.public": "Fratara tempolinio",
-  "column.status": "Toot",
+  "column.status": "Mesaĝo",
   "column_back_button.label": "Reveni",
   "column_header.hide_settings": "Kaŝi agordojn",
   "column_header.moveLeft_settings": "Movi kolumnon maldekstren",
@@ -79,10 +79,10 @@
   "compose_form.lock_disclaimer": "Via konta ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn, kiuj estas nur por sekvantoj.",
   "compose_form.lock_disclaimer.lock": "ŝlosita",
   "compose_form.placeholder": "Pri kio vi pensas?",
-  "compose_form.poll.add_option": "Aldoni elekto",
+  "compose_form.poll.add_option": "Aldoni elekteblon",
   "compose_form.poll.duration": "Balotenketa daŭro",
-  "compose_form.poll.option_placeholder": "elekto {number}",
-  "compose_form.poll.remove_option": "Forigi ĉi tiu elekton",
+  "compose_form.poll.option_placeholder": "Elekteblo {number}",
+  "compose_form.poll.remove_option": "Forigi ĉi tiu elekteblon",
   "compose_form.publish": "Hup",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.hide": "Marki la aŭdovidaĵojn kiel tiklaj",
@@ -92,7 +92,7 @@
   "compose_form.spoiler.unmarked": "Teksto ne kaŝita",
   "compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie",
   "confirmation_modal.cancel": "Nuligi",
-  "confirmations.block.block_and_report": "Bloki & Signali",
+  "confirmations.block.block_and_report": "Bloki kaj signali",
   "confirmations.block.confirm": "Bloki",
   "confirmations.block.message": "Ĉu vi certas, ke vi volas bloki {name}?",
   "confirmations.delete.confirm": "Forigi",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Respondi nun anstataŭigos la mesaĝon, kiun vi nun skribas. Ĉu vi certas, ke vi volas daŭrigi?",
   "confirmations.unfollow.confirm": "Ne plu sekvi",
   "confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?",
+  "conversation.delete": "Forigi konversacion",
+  "conversation.mark_as_read": "Marki legita",
+  "conversation.open": "Vidi konversacion",
+  "conversation.with": "Kun {names}",
   "directory.federated": "El konata fediverso",
   "directory.local": "Nur de {domain}",
-  "directory.new_arrivals": "Novaj veniĝoj",
-  "directory.recently_active": "Recently active",
+  "directory.new_arrivals": "Novaj alvenoj",
+  "directory.recently_active": "Lastatempe aktiva",
   "embed.instructions": "Enkorpigu ĉi tiun mesaĝon en vian retejon per kopio de la suba kodo.",
   "embed.preview": "Ĝi aperos tiel:",
   "emoji_button.activity": "Agadoj",
@@ -166,18 +170,18 @@
   "hashtag.column_settings.tag_mode.all": "Ĉiuj",
   "hashtag.column_settings.tag_mode.any": "Iu ajn",
   "hashtag.column_settings.tag_mode.none": "Neniu",
-  "hashtag.column_settings.tag_toggle": "Inkluzivi pluajn etikedojn por ĉi tiu kolumno",
+  "hashtag.column_settings.tag_toggle": "Aldoni pliajn etikedojn por ĉi tiu kolumno",
   "home.column_settings.basic": "Bazaj agordoj",
   "home.column_settings.show_reblogs": "Montri diskonigojn",
   "home.column_settings.show_replies": "Montri respondojn",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Tuje ĝisdatigi",
   "intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}",
   "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}",
   "introduction.federation.action": "Sekva",
-  "introduction.federation.federated.headline": "Federacio",
+  "introduction.federation.federated.headline": "Fratara",
   "introduction.federation.federated.text": "Publikaj mesaĝoj el aliaj serviloj de la Fediverse aperos en la fratara tempolinio.",
-  "introduction.federation.home.headline": "Heimo",
+  "introduction.federation.home.headline": "Hejmo",
   "introduction.federation.home.text": "Mesaĝoj de homoj, kiujn vi sekvas, aperos en via hejma fluo. Vi povas sekvi iun ajn de ajna servilo!",
   "introduction.federation.local.headline": "Loka",
   "introduction.federation.local.text": "Publikaj mesaĝoj de homoj de via servilo aperos en la loka tempolinio.",
@@ -226,7 +230,7 @@
   "lightbox.close": "Fermi",
   "lightbox.next": "Sekva",
   "lightbox.previous": "Antaŭa",
-  "lightbox.view_context": "Vidi kontekston",
+  "lightbox.view_context": "Vidi kuntekston",
   "lists.account.add": "Aldoni al la listo",
   "lists.account.remove": "Forigi de la listo",
   "lists.delete": "Forigi la liston",
@@ -236,12 +240,12 @@
   "lists.new.title_placeholder": "Titolo de la nova listo",
   "lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
   "lists.subheading": "Viaj listoj",
-  "load_pending": "{count,plural, one {# nova ero} other {# novaj eroj}}",
+  "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
   "loading_indicator.label": "Ŝargado…",
   "media_gallery.toggle_visible": "Baskuligi videblecon",
   "missing_indicator.label": "Ne trovita",
   "missing_indicator.sublabel": "Ĉi tiu elemento ne estis trovita",
-  "mute_modal.hide_notifications": "Ĉu vi volas kaŝi la sciigojn el ĉi tiu uzanto?",
+  "mute_modal.hide_notifications": "Ĉu vi volas kaŝi la sciigojn de ĉi tiu uzanto?",
   "navigation_bar.apps": "Telefonaj aplikaĵoj",
   "navigation_bar.blocks": "Blokitaj uzantoj",
   "navigation_bar.community_timeline": "Loka tempolinio",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Preferoj",
   "navigation_bar.public_timeline": "Fratara tempolinio",
   "navigation_bar.security": "Sekureco",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "kaj {count, plural, one {# alia} other {# aliaj}}",
   "notification.favourite": "{name} stelumis vian mesaĝon",
   "notification.follow": "{name} eksekvis vin",
   "notification.mention": "{name} menciis vin",
@@ -295,6 +299,7 @@
   "poll.refresh": "Aktualigi",
   "poll.total_votes": "{count, plural, one {# voĉdono} other {# voĉdonoj}}",
   "poll.vote": "Voĉdoni",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Aldoni balotenketon",
   "poll_button.remove_poll": "Forigi balotenketon",
   "privacy.change": "Agordi mesaĝan privatecon",
@@ -335,7 +340,7 @@
   "status.admin_account": "Malfermi la kontrolan interfacon por @{name}",
   "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco",
   "status.block": "Bloki @{name}",
-  "status.cancel_reblog_private": "Eksdiskonigi",
+  "status.cancel_reblog_private": "Ne plu diskonigi",
   "status.cannot_reblog": "Ĉi tiu mesaĝo ne diskonigeblas",
   "status.copy": "Kopii la ligilon al la mesaĝo",
   "status.delete": "Forigi",
@@ -394,19 +399,19 @@
   "upload_form.description": "Priskribi por misvidantaj homoj",
   "upload_form.edit": "Redakti",
   "upload_form.undo": "Forigi",
-  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.analyzing_picture": "Bilda analizado…",
   "upload_modal.apply": "Apliki",
-  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
-  "upload_modal.detect_text": "Detect text from picture",
-  "upload_modal.edit_media": "Redakti aŭdvidaĵo",
-  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.description_placeholder": "Laŭ Ludoviko Zamenhof bongustas freŝa ĉeĥa manĝaĵo kun spicoj",
+  "upload_modal.detect_text": "Detekti tekston de la bildo",
+  "upload_modal.edit_media": "Redakti aŭdovidaĵon",
+  "upload_modal.hint": "Klaku aŭ trenu la cirklon en la antaŭvidilo por elekti la fokuspunkton kiu ĉiam videblos en ĉiuj etigitaj bildoj.",
   "upload_modal.preview_label": "Antaŭvido ({ratio})",
   "upload_progress.label": "Alŝutado…",
-  "video.close": "Fermi videon",
+  "video.close": "Fermi la videon",
   "video.exit_fullscreen": "Eksigi plenekrana",
-  "video.expand": "Grandigi videon",
+  "video.expand": "Grandigi la videon",
   "video.fullscreen": "Igi plenekrana",
-  "video.hide": "Kaŝi videon",
+  "video.hide": "Kaŝi la videon",
   "video.mute": "Silentigi",
   "video.pause": "Paŭzi",
   "video.play": "Ekigi",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
new file mode 100644
index 000000000..89ddcbf51
--- /dev/null
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -0,0 +1,419 @@
+{
+  "account.add_or_remove_from_list": "Agregar o quitar de las listas",
+  "account.badges.bot": "Bot",
+  "account.block": "Bloquear a @{name}",
+  "account.block_domain": "Ocultar todo de {domain}",
+  "account.blocked": "Bloqueado",
+  "account.cancel_follow_request": "Cancelar la solicitud de seguimiento",
+  "account.direct": "Mensaje directo a @{name}",
+  "account.domain_blocked": "Dominio oculto",
+  "account.edit_profile": "Editar perfil",
+  "account.endorse": "Destacar en el perfil",
+  "account.follow": "Seguir",
+  "account.followers": "Seguidores",
+  "account.followers.empty": "Todavía nadie sigue a este usuario.",
+  "account.follows": "Sigue",
+  "account.follows.empty": "Todavía este usuario no sigue a nadie.",
+  "account.follows_you": "Te sigue",
+  "account.hide_reblogs": "Ocultar retoots de @{name}",
+  "account.last_status": "Última actividad",
+  "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}",
+  "account.locked_info": "El estado de privacidad de esta cuenta está establecido como bloqueado. El propietario manualmente revisa quién puede seguirle.",
+  "account.media": "Medios",
+  "account.mention": "Mencionar a @{name}",
+  "account.moved_to": "{name} se ha muó a:",
+  "account.mute": "Silenciar a @{name}",
+  "account.mute_notifications": "Silenciar notificaciones de @{name}",
+  "account.muted": "Silenciado",
+  "account.never_active": "Nunca",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots con respuestas",
+  "account.report": "Denunciar a @{name}",
+  "account.requested": "Esperando aprobación. Hacé clic para cancelar la solicitud de seguimiento.",
+  "account.share": "Compartir el perfil de @{name}",
+  "account.show_reblogs": "Mostrar retoots de @{name}",
+  "account.unblock": "Desbloquear a @{name}",
+  "account.unblock_domain": "Mostrar {domain}",
+  "account.unendorse": "No destacar en el perfil",
+  "account.unfollow": "Dejar de seguir",
+  "account.unmute": "Dejar de silenciar a @{name}",
+  "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
+  "alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Tarifa limitada",
+  "alert.unexpected.message": "Ocurrió un error inesperado.",
+  "alert.unexpected.title": "¡Epa!",
+  "autosuggest_hashtag.per_week": "{count} por semana",
+  "boost_modal.combo": "Podés hacer clic en {combo} para saltar esto la próxima vez",
+  "bundle_column_error.body": "Algo salió mal al cargar este componente.",
+  "bundle_column_error.retry": "Intentá de nuevo",
+  "bundle_column_error.title": "Error de red",
+  "bundle_modal_error.close": "Cerrar",
+  "bundle_modal_error.message": "Algo salió mal al cargar este componente.",
+  "bundle_modal_error.retry": "Intentá de nuevo",
+  "column.blocks": "Usuarios bloqueados",
+  "column.community": "Línea temporal local",
+  "column.direct": "Mensajes directos",
+  "column.directory": "Explorar perfiles",
+  "column.domain_blocks": "Dominios ocultos",
+  "column.favourites": "Favoritos",
+  "column.follow_requests": "Solicitudes de seguimiento",
+  "column.home": "Principal",
+  "column.lists": "Listas",
+  "column.mutes": "Usuarios silenciados",
+  "column.notifications": "Notificaciones",
+  "column.pins": "Toots fijados",
+  "column.public": "Línea temporal federada",
+  "column.status": "Toot",
+  "column_back_button.label": "Volver",
+  "column_header.hide_settings": "Ocultar configuración",
+  "column_header.moveLeft_settings": "Mover columna a la izquierda",
+  "column_header.moveRight_settings": "Mover columna a la derecha",
+  "column_header.pin": "Fijar",
+  "column_header.show_settings": "Mostrar configuración",
+  "column_header.unpin": "Dejar de fijar",
+  "column_subheading.settings": "Configuración",
+  "community.column_settings.media_only": "Sólo medios",
+  "compose_form.direct_message_warning": "Este toot sólo será enviado a los usuarios mencionados.",
+  "compose_form.direct_message_warning_learn_more": "Aprendé más",
+  "compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.",
+  "compose_form.lock_disclaimer": "Tu cuenta no está {locked}. Todos pueden seguirte para ver tus toots marcados como \"sólo para seguidores\".",
+  "compose_form.lock_disclaimer.lock": "bloqueada",
+  "compose_form.placeholder": "¿Qué onda?",
+  "compose_form.poll.add_option": "Agregá una opción",
+  "compose_form.poll.duration": "Duración de la encuesta",
+  "compose_form.poll.option_placeholder": "Opción {number}",
+  "compose_form.poll.remove_option": "Quitá esta opción",
+  "compose_form.publish": "Tootear",
+  "compose_form.publish_loud": "¡{publish}!",
+  "compose_form.sensitive.hide": "Marcar medio como sensible",
+  "compose_form.sensitive.marked": "El medio se marcó como sensible",
+  "compose_form.sensitive.unmarked": "El medio no está marcado como sensible",
+  "compose_form.spoiler.marked": "El texto está oculto detrás de la advertencia",
+  "compose_form.spoiler.unmarked": "El texto no está oculto",
+  "compose_form.spoiler_placeholder": "Escribí tu advertencia acá",
+  "confirmation_modal.cancel": "Cancelar",
+  "confirmations.block.block_and_report": "Bloquear y denunciar",
+  "confirmations.block.confirm": "Bloquear",
+  "confirmations.block.message": "¿Estás seguro que querés bloquear a {name}?",
+  "confirmations.delete.confirm": "Eliminar",
+  "confirmations.delete.message": "¿Estás seguro que querés eliminar este estado?",
+  "confirmations.delete_list.confirm": "Eliminar",
+  "confirmations.delete_list.message": "¿Estás seguro que querés eliminar permanentemente esta lista?",
+  "confirmations.domain_block.confirm": "Ocultar dominio entero",
+  "confirmations.domain_block.message": "¿Estás completamente seguro que querés bloquear el {domain} entero? En la mayoría de los casos, unos cuantos bloqueos y silenciados puntuales son suficientes y preferibles. No vas a ver contenido de ese dominio en ninguna de tus líneas temporales o en tus notificaciones. Tus seguidores de ese dominio serán quitados.",
+  "confirmations.logout.confirm": "Cerrar sesión",
+  "confirmations.logout.message": "¿Estás seguro que querés cerrar la sesión?",
+  "confirmations.mute.confirm": "Silenciar",
+  "confirmations.mute.message": "¿Estás seguro que querés silenciar a {name}?",
+  "confirmations.redraft.confirm": "Eliminar toot original y editarlo",
+  "confirmations.redraft.message": "¿Estás seguro que querés eliminar este estado y volverlo a editarlo? Se perderán las veces marcadas como favoritos y los retoots, y las respuestas a la publicación original quedarán huérfanas.",
+  "confirmations.reply.confirm": "Responder",
+  "confirmations.reply.message": "Responder ahora sobreescribirá el mensaje que estás redactando actualmente. ¿Estás seguro que querés seguir?",
+  "confirmations.unfollow.confirm": "Dejar de seguir",
+  "confirmations.unfollow.message": "¿Estás seguro que querés dejar de seguir a {name}?",
+  "conversation.delete": "Eliminar conversación",
+  "conversation.mark_as_read": "Marcar como leído",
+  "conversation.open": "Ver conversación",
+  "conversation.with": "Con {names}",
+  "directory.federated": "Desde fediverso conocido",
+  "directory.local": "Sólo de {domain}",
+  "directory.new_arrivals": "Recién llegados",
+  "directory.recently_active": "Recientemente activo",
+  "embed.instructions": "Insertá este toot a tu sitio web copiando el código de abajo.",
+  "embed.preview": "Así es cómo se verá:",
+  "emoji_button.activity": "Actividad",
+  "emoji_button.custom": "Personalizado",
+  "emoji_button.flags": "Banderas",
+  "emoji_button.food": "Comida y bebida",
+  "emoji_button.label": "Insertar emoji",
+  "emoji_button.nature": "Naturaleza",
+  "emoji_button.not_found": "¡¡No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objetos",
+  "emoji_button.people": "Gente",
+  "emoji_button.recent": "Usados frecuentemente",
+  "emoji_button.search": "Buscar…",
+  "emoji_button.search_results": "Resultados de búsqueda",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viajes y lugares",
+  "empty_column.account_timeline": "¡No hay toots aquí!",
+  "empty_column.account_unavailable": "Perfil no disponible",
+  "empty_column.blocks": "Todavía no bloqueaste a ningún usuario.",
+  "empty_column.community": "La línea temporal local está vacía. ¡Escribí algo en modo público para que se empiece a correr la bola!",
+  "empty_column.direct": "Todavía no tenés ningún mensaje directo. Cuando enviés o recibás uno, se mostrará acá.",
+  "empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
+  "empty_column.favourited_statuses": "Todavía no tenés toots favoritos. Cuando marqués uno como favorito, se mostrará acá.",
+  "empty_column.favourites": "Todavía nadie marcó este toot como favorito. Cuando alguien lo haga, se mostrará acá.",
+  "empty_column.follow_requests": "Todavía no tenés ninguna solicitud de seguimiento. Cuando recibás una, se mostrará acá.",
+  "empty_column.hashtag": "Todavía no hay nada con esta etiqueta.",
+  "empty_column.home": "¡Tu línea temporal principal está vacía! Visitá {public} o usá la búsqueda para comenzar y encontrar a otros usuarios.",
+  "empty_column.home.public_timeline": "la línea temporal pública",
+  "empty_column.list": "Todavía no hay nada en esta lista. Cuando miembros de esta lista envíen nuevos toots, se mostrarán acá.",
+  "empty_column.lists": "Todavía no tienes ninguna lista. Cuando creés una, se mostrará acá.",
+  "empty_column.mutes": "Todavía no silenciaste a ningún usuario.",
+  "empty_column.notifications": "Todavía no tenés ninguna notificación. Interactuá con otros para iniciar la conversación.",
+  "empty_column.public": "¡Naranja! Escribí algo públicamente, o seguí usuarios manualmente de otros servidores para ir llenando esta línea temporal.",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rechazar",
+  "getting_started.developers": "Desarrolladores",
+  "getting_started.directory": "Directorio de perfiles",
+  "getting_started.documentation": "Documentación",
+  "getting_started.heading": "Introducción",
+  "getting_started.invite": "Invitar usuarios",
+  "getting_started.open_source_notice": "Mastodon es software libre. Podés contribuir o informar errores en {github}.",
+  "getting_started.security": "Seguridad",
+  "getting_started.terms": "Términos del servicio",
+  "hashtag.column_header.tag_mode.all": "y {additional}",
+  "hashtag.column_header.tag_mode.any": "o {additional}",
+  "hashtag.column_header.tag_mode.none": "sin {additional}",
+  "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias",
+  "hashtag.column_settings.select.placeholder": "Introducí etiquetas…",
+  "hashtag.column_settings.tag_mode.all": "Todas estas",
+  "hashtag.column_settings.tag_mode.any": "Cualquiera de estas",
+  "hashtag.column_settings.tag_mode.none": "Ninguna de estas",
+  "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionales para esta columna",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.show_reblogs": "Mostrar retoots",
+  "home.column_settings.show_replies": "Mostrar respuestas",
+  "home.column_settings.update_live": "Actualizar en tiempo real",
+  "intervals.full.days": "{number, plural, one {# día} other {# días}}",
+  "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
+  "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
+  "introduction.federation.action": "Siguiente",
+  "introduction.federation.federated.headline": "Federado",
+  "introduction.federation.federated.text": "Los toots públicos de otros servidores del fediverso aparecerán en la línea temporal federada.",
+  "introduction.federation.home.headline": "Principal",
+  "introduction.federation.home.text": "Los toots de las personas que seguíss aparecerán en tu línea temporal principal. ¡Podés seguir a cualquiera en cualquier servidor!",
+  "introduction.federation.local.headline": "Local",
+  "introduction.federation.local.text": "Los toots públicos de las personas en el mismo servidor aparecerán en la línea temporal local.",
+  "introduction.interactions.action": "¡Terminar tutorial!",
+  "introduction.interactions.favourite.headline": "Favorito",
+  "introduction.interactions.favourite.text": "Podés guardar un toot para más tarde, y hacerle saber al autor que te gustó, marcándolo como favorito.",
+  "introduction.interactions.reblog.headline": "Retootear",
+  "introduction.interactions.reblog.text": "Podés compartir los toots de otras personas con tus seguidores retooteando los mismos.",
+  "introduction.interactions.reply.headline": "Responder",
+  "introduction.interactions.reply.text": "Podés responder a tus propios toots y los de otras personas, que se encadenarán juntos en una conversación.",
+  "introduction.welcome.action": "¡Dale!",
+  "introduction.welcome.headline": "Primeros pasos",
+  "introduction.welcome.text": "¡Bienvenido al fediverso! En unos pocos minutos, vas a poder transmitir mensajes y hablar con tus amigos a través de una amplia variedad de servidores. Pero este servidor, {domain}, es especial: aloja tu perfil, así que acordate de su nombre.",
+  "keyboard_shortcuts.back": "para volver",
+  "keyboard_shortcuts.blocked": "para abrir la lista de usuarios bloqueados",
+  "keyboard_shortcuts.boost": "para retootear",
+  "keyboard_shortcuts.column": "para enfocar un estado en una de las columnas",
+  "keyboard_shortcuts.compose": "para enfocar el área de texto de redacción",
+  "keyboard_shortcuts.description": "Descripción",
+  "keyboard_shortcuts.direct": "para abrir columna de mensajes directos",
+  "keyboard_shortcuts.down": "para bajar en la lista",
+  "keyboard_shortcuts.enter": "para abrir el estado",
+  "keyboard_shortcuts.favourite": "para marcar como favorito",
+  "keyboard_shortcuts.favourites": "para abrir la lista de favoritos",
+  "keyboard_shortcuts.federated": "para abrir la línea temporal federada",
+  "keyboard_shortcuts.heading": "Atajos de teclado",
+  "keyboard_shortcuts.home": "para abrir la línea temporal principal",
+  "keyboard_shortcuts.hotkey": "Combinación",
+  "keyboard_shortcuts.legend": "para mostrar este texto",
+  "keyboard_shortcuts.local": "para abrir la línea temporal local",
+  "keyboard_shortcuts.mention": "para mencionar al autor",
+  "keyboard_shortcuts.muted": "abrir la lista de usuarios silenciados",
+  "keyboard_shortcuts.my_profile": "para abrir tu perfil",
+  "keyboard_shortcuts.notifications": "para abrir la columna de notificaciones",
+  "keyboard_shortcuts.pinned": "para abrir lista de toots fijados",
+  "keyboard_shortcuts.profile": "para abrir el perfil del autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.requests": "para abrir la lista de solicitudes de seguimiento",
+  "keyboard_shortcuts.search": "para enfocar la búsqueda",
+  "keyboard_shortcuts.start": "para abrir la columna \"Introducción\"",
+  "keyboard_shortcuts.toggle_hidden": "para mostrar/ocultar el texto detrás de la advertencia de contenido",
+  "keyboard_shortcuts.toggle_sensitivity": "para mostrar/ocultar los medios",
+  "keyboard_shortcuts.toot": "para comenzar un toot nuevo",
+  "keyboard_shortcuts.unfocus": "para quitar el enfoque del área de texto de redacción o de búsqueda",
+  "keyboard_shortcuts.up": "para subir en la lista",
+  "lightbox.close": "Cerrar",
+  "lightbox.next": "Siguiente",
+  "lightbox.previous": "Anterior",
+  "lightbox.view_context": "Ver contexto",
+  "lists.account.add": "Agregar a lista",
+  "lists.account.remove": "Quitar de lista",
+  "lists.delete": "Eliminar lista",
+  "lists.edit": "Editar lista",
+  "lists.edit.submit": "Cambiar título",
+  "lists.new.create": "Agregar lista",
+  "lists.new.title_placeholder": "Nuevo título de lista",
+  "lists.search": "Buscar entre la gente que seguís",
+  "lists.subheading": "Tus listas",
+  "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
+  "loading_indicator.label": "Cargando…",
+  "media_gallery.toggle_visible": "Cambiar visibilidad",
+  "missing_indicator.label": "No se encontró",
+  "missing_indicator.sublabel": "No se encontró este recurso",
+  "mute_modal.hide_notifications": "¿Querés ocultar las notificaciones de este usuario?",
+  "navigation_bar.apps": "Aplicaciones móviles",
+  "navigation_bar.blocks": "Usuarios bloqueados",
+  "navigation_bar.community_timeline": "Línea temporal local",
+  "navigation_bar.compose": "Redactar un nuevo toot",
+  "navigation_bar.direct": "Mensajes directos",
+  "navigation_bar.discover": "Descubrir",
+  "navigation_bar.domain_blocks": "Dominios ocultos",
+  "navigation_bar.edit_profile": "Editar perfil",
+  "navigation_bar.favourites": "Favoritos",
+  "navigation_bar.filters": "Palabras silenciadas",
+  "navigation_bar.follow_requests": "Solicitudes de seguimiento",
+  "navigation_bar.follows_and_followers": "Personas seguidas y seguidores",
+  "navigation_bar.info": "Acerca de este servidor",
+  "navigation_bar.keyboard_shortcuts": "Atajos",
+  "navigation_bar.lists": "Listas",
+  "navigation_bar.logout": "Cerrar sesión",
+  "navigation_bar.mutes": "Usuarios silenciados",
+  "navigation_bar.personal": "Personal",
+  "navigation_bar.pins": "Toots fijados",
+  "navigation_bar.preferences": "Configuración",
+  "navigation_bar.public_timeline": "Línea temporal federada",
+  "navigation_bar.security": "Seguridad",
+  "notification.and_n_others": "y {count, plural, one {# otro} other {# otros}}",
+  "notification.favourite": "{name} marcó tu estado como favorito",
+  "notification.follow": "{name} te empezó a seguir",
+  "notification.mention": "{name} te mencionó",
+  "notification.poll": "Finalizó una encuesta en la que votaste",
+  "notification.reblog": "{name} retooteó tu estado",
+  "notifications.clear": "Limpiar notificaciones",
+  "notifications.clear_confirmation": "¿Estás seguro que querés limpiar todas tus notificaciones permanentemente?",
+  "notifications.column_settings.alert": "Notificaciones de escritorio",
+  "notifications.column_settings.favourite": "Favoritos:",
+  "notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
+  "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
+  "notifications.column_settings.filter_bar.show": "Mostrar",
+  "notifications.column_settings.follow": "Nuevos seguidores:",
+  "notifications.column_settings.mention": "Menciones:",
+  "notifications.column_settings.poll": "Resultados de la encuesta:",
+  "notifications.column_settings.push": "Notificaciones push",
+  "notifications.column_settings.reblog": "Retoots:",
+  "notifications.column_settings.show": "Mostrar en columna",
+  "notifications.column_settings.sound": "Reproducir sonido",
+  "notifications.filter.all": "Todas",
+  "notifications.filter.boosts": "Retoots",
+  "notifications.filter.favourites": "Favoritos",
+  "notifications.filter.follows": "Seguidores",
+  "notifications.filter.mentions": "Menciones",
+  "notifications.filter.polls": "Resultados de la encuesta",
+  "notifications.group": "{count} notificaciones",
+  "poll.closed": "Cerrada",
+  "poll.refresh": "Refrescar",
+  "poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
+  "poll.vote": "Votar",
+  "poll.voted": "You voted for this answer",
+  "poll_button.add_poll": "Add a poll",
+  "poll_button.remove_poll": "Remove poll",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {target}",
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Report {target}",
+  "search.placeholder": "Search",
+  "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "user",
+  "search_results.accounts": "People",
+  "search_results.hashtags": "Hashtags",
+  "search_results.statuses": "Toots",
+  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.copy": "Copy link to status",
+  "status.delete": "Delete",
+  "status.detailed_status": "Detailed conversation view",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "status.filtered": "Filtered",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.more": "More",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Mute conversation",
+  "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.read_more": "Read more",
+  "status.reblog": "Boost",
+  "status.reblog_private": "Boost to original audience",
+  "status.reblogged_by": "{name} boosted",
+  "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
+  "status.redraft": "Delete & re-draft",
+  "status.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
+  "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
+  "status.show_thread": "Show thread",
+  "status.uncached_media_warning": "Not available",
+  "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "suggestions.dismiss": "Dismiss suggestion",
+  "suggestions.header": "You might be interested in…",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.trending_now": "Trending now",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media ({formats})",
+  "upload_error.limit": "File upload limit exceeded.",
+  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.edit": "Edit",
+  "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index a033f6e1f..63be0e4c3 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -38,11 +38,11 @@
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Dejar de silenciar a @{name}",
   "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
+  "alert.rate_limited.message": "Por favor reintente después de {retry_time, time, medium}.",
   "alert.rate_limited.title": "Tarifa limitada",
   "alert.unexpected.message": "Hubo un error inesperado.",
   "alert.unexpected.title": "¡Ups!",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "autosuggest_hashtag.per_week": "{count} por semana",
   "boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez",
   "bundle_column_error.body": "Algo salió mal al cargar este componente.",
   "bundle_column_error.retry": "Inténtalo de nuevo",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?",
   "confirmations.unfollow.confirm": "Dejar de seguir",
   "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
+  "conversation.delete": "Borrar conversación",
+  "conversation.mark_as_read": "Marcar como leído",
+  "conversation.open": "Ver conversación",
+  "conversation.with": "Con {names}",
   "directory.federated": "Desde el fediverso conocido",
   "directory.local": "Sólo de {domain}",
   "directory.new_arrivals": "Recién llegados",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar retoots",
   "home.column_settings.show_replies": "Mostrar respuestas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualizar en tiempo real",
   "intervals.full.days": "{number, plural, one {# día} other {# días}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Preferencias",
   "navigation_bar.public_timeline": "Historia federada",
   "navigation_bar.security": "Seguridad",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "y {count, plural, one {# otro} other {# otros}}",
   "notification.favourite": "{name} marcó tu estado como favorito",
   "notification.follow": "{name} te empezó a seguir",
   "notification.mention": "{name} te ha mencionado",
@@ -295,6 +299,7 @@
   "poll.refresh": "Actualizar",
   "poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
   "poll.vote": "Votar",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Añadir una encuesta",
   "poll_button.remove_poll": "Eliminar encuesta",
   "privacy.change": "Ajustar privacidad",
@@ -400,7 +405,7 @@
   "upload_modal.detect_text": "Detectar texto de la imagen",
   "upload_modal.edit_media": "Editar multimedia",
   "upload_modal.hint": "Haga clic o arrastre el círculo en la vista previa para elegir el punto focal que siempre estará a la vista en todas las miniaturas.",
-  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_modal.preview_label": "Vista previa ({ratio})",
   "upload_progress.label": "Subiendo…",
   "video.close": "Cerrar video",
   "video.exit_fullscreen": "Salir de pantalla completa",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index 1d1dfd35a..5d3b080de 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Kohene vastamine kirjutab üle sõnumi, mida hetkel koostad. Oled kindel, et soovid jätkata?",
   "confirmations.unfollow.confirm": "Ära jälgi",
   "confirmations.unfollow.message": "Oled kindel, et ei soovi jälgida {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Värskenda",
   "poll.total_votes": "{count, plural, one {# hääl} other {# hääli}}",
   "poll.vote": "Hääleta",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Lisa küsitlus",
   "poll_button.remove_poll": "Eemalda küsitlus",
   "privacy.change": "Muuda staatuse privaatsust",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index f1fc17fdd..0d272c223 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Orain erantzuteak idazten ari zaren mezua gainidatziko du. Ziur jarraitu nahi duzula?",
   "confirmations.unfollow.confirm": "Utzi jarraitzeari",
   "confirmations.unfollow.message": "Ziur {name} jarraitzeari utzi nahi diozula?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "Fedibertso ezagunekoak",
   "directory.local": "{domain} domeinukoak soilik",
   "directory.new_arrivals": "Iritsi berriak",
@@ -295,6 +299,7 @@
   "poll.refresh": "Berritu",
   "poll.total_votes": "{count, plural, one {boto #} other {# boto}}",
   "poll.vote": "Bozkatu",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Gehitu inkesta bat",
   "poll_button.remove_poll": "Kendu inkesta",
   "privacy.change": "Doitu mezuaren pribatutasuna",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 9382ec5ee..70c1734cb 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -3,7 +3,7 @@
   "account.badges.bot": "ربات",
   "account.block": "مسدودسازی @{name}",
   "account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
-  "account.blocked": "مسدودشده",
+  "account.blocked": "مسدود شده",
   "account.cancel_follow_request": "لغو درخواست پیگیری",
   "account.direct": "پیغام خصوصی به @{name}",
   "account.domain_blocked": "دامین پنهان‌شده",
@@ -16,7 +16,7 @@
   "account.follows.empty": "این کاربر هنوز هیچ کسی را پی نمی‌گیرد.",
   "account.follows_you": "پیگیر شماست",
   "account.hide_reblogs": "پنهان کردن بازبوق‌های @{name}",
-  "account.last_status": "Last active",
+  "account.last_status": "آخرین فعالیت",
   "account.link_verified_on": "مالکیت این نشانی در تاریخ {date} بررسی شد",
   "account.locked_info": "این حساب خصوصی است. صاحب این حساب تصمیم می‌گیرد که چه کسی می‌تواند پیگیرش باشد.",
   "account.media": "عکس و ویدیو",
@@ -25,7 +25,7 @@
   "account.mute": "بی‌صدا کردن @{name}",
   "account.mute_notifications": "بی‌صداکردن اعلان‌ها از طرف @{name}",
   "account.muted": "بی‌صداشده",
-  "account.never_active": "Never",
+  "account.never_active": "هرگز",
   "account.posts": "نوشته‌ها",
   "account.posts_with_replies": "نوشته‌ها و پاسخ‌ها",
   "account.report": "گزارش @{name}",
@@ -38,8 +38,8 @@
   "account.unfollow": "پایان پیگیری",
   "account.unmute": "باصدا کردن @{name}",
   "account.unmute_notifications": "باصداکردن اعلان‌ها از طرف @{name}",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "لطفاً پس از {retry_time, time, medium} دوباره تلاش کنید.",
+  "alert.rate_limited.title": "محدودیت تعداد",
   "alert.unexpected.message": "خطای پیش‌بینی‌نشده‌ای رخ داد.",
   "alert.unexpected.title": "ای وای!",
   "autosuggest_hashtag.per_week": "{count} در هفته",
@@ -53,7 +53,7 @@
   "column.blocks": "کاربران مسدودشده",
   "column.community": "نوشته‌های محلی",
   "column.direct": "پیغام‌های خصوصی",
-  "column.directory": "Browse profiles",
+  "column.directory": "مرور نمایه‌ها",
   "column.domain_blocks": "دامین‌های پنهان‌شده",
   "column.favourites": "پسندیده‌ها",
   "column.follow_requests": "درخواست‌های پیگیری",
@@ -63,7 +63,7 @@
   "column.notifications": "اعلان‌ها",
   "column.pins": "نوشته‌های ثابت",
   "column.public": "نوشته‌های همه‌جا",
-  "column.status": "Toot",
+  "column.status": "بوق",
   "column_back_button.label": "بازگشت",
   "column_header.hide_settings": "نهفتن تنظیمات",
   "column_header.moveLeft_settings": "انتقال ستون به راست",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "آیا واقعاً می‌خواهید این فهرست را برای همیشه پاک کنید؟",
   "confirmations.domain_block.confirm": "پنهان‌سازی کل دامین",
   "confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود. پس از این کار شما هیچ نوشته‌ای را از این دامین در فهرست نوشته‌های عمومی یا اعلان‌هایتان نخواهید دید. پیگیران شما از این دامین هم حذف خواهد شد.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "خروج",
+  "confirmations.logout.message": "آیا مطمئنید که می‌خواهید خارج شوید؟",
   "confirmations.mute.confirm": "بی‌صدا کن",
   "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
   "confirmations.redraft.confirm": "پاک‌کردن و بازنویسی",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "اگر الان پاسخ دهید، چیزی که در حال نوشتنش بودید پاک خواهد شد. آیا همین را می‌خواهید؟",
   "confirmations.unfollow.confirm": "لغو پیگیری",
   "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "حذف گفتگو",
+  "conversation.mark_as_read": "علامت‌گذاری به عنوان خوانده شده",
+  "conversation.open": "دیدن گفتگو",
+  "conversation.with": "با {names}",
+  "directory.federated": "از سرورهای همسایه",
+  "directory.local": "تنها از {domain}",
+  "directory.new_arrivals": "تازه‌واردان",
+  "directory.recently_active": "کاربران فعال اخیر",
   "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
   "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
   "emoji_button.activity": "فعالیت",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "اصلی",
   "home.column_settings.show_reblogs": "نمایش بازبوق‌ها",
   "home.column_settings.show_replies": "نمایش پاسخ‌ها",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "به‌روزرسانی لحظه‌ای",
   "intervals.full.days": "{number, plural, one {# روز} other {# روز}}",
   "intervals.full.hours": "{number, plural, one {# ساعت} other {# ساعت}}",
   "intervals.full.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "ترجیحات",
   "navigation_bar.public_timeline": "نوشته‌های همه‌جا",
   "navigation_bar.security": "امنیت",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "و {count, plural, one {# اعلان دیگر} other {# اعلان دیگر}}",
   "notification.favourite": "‫{name}‬ نوشتهٔ شما را پسندید",
   "notification.follow": "‫{name}‬ پیگیر شما شد",
   "notification.mention": "‫{name}‬ از شما نام برد",
@@ -295,6 +299,7 @@
   "poll.refresh": "به‌روزرسانی",
   "poll.total_votes": "{count, plural, one {# رأی} other {# رأی}}",
   "poll.vote": "رأی",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "افزودن نظرسنجی",
   "poll_button.remove_poll": "حذف نظرسنجی",
   "privacy.change": "تنظیم حریم خصوصی نوشته‌ها",
@@ -369,7 +374,7 @@
   "status.show_more": "نمایش",
   "status.show_more_all": "نمایش بیشتر همه",
   "status.show_thread": "نمایش گفتگو",
-  "status.uncached_media_warning": "Not available",
+  "status.uncached_media_warning": "ناموجود",
   "status.unmute_conversation": "باصداکردن گفتگو",
   "status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
   "suggestions.dismiss": "پیشنهاد را نادیده بگیر",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 01b5edad1..93b770fca 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Jos vastaat nyt, vastaus korvaa tällä hetkellä työstämäsi viestin. Oletko varma, että haluat jatkaa?",
   "confirmations.unfollow.confirm": "Lakkaa seuraamasta",
   "confirmations.unfollow.message": "Haluatko varmasti lakata seuraamasta käyttäjää {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Säädä tuuttauksen näkyvyyttä",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 7cfe9829a..5120d6bcc 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -39,7 +39,7 @@
   "account.unmute": "Ne plus masquer @{name}",
   "account.unmute_notifications": "Réactiver les notifications de @{name}",
   "alert.rate_limited.message": "Veuillez réessayer après {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Taux limité",
+  "alert.rate_limited.title": "Débit limité",
   "alert.unexpected.message": "Une erreur inattendue s’est produite.",
   "alert.unexpected.title": "Oups !",
   "autosuggest_hashtag.per_week": "{count} par semaine",
@@ -63,19 +63,19 @@
   "column.notifications": "Notifications",
   "column.pins": "Pouets épinglés",
   "column.public": "Fil public global",
-  "column.status": "Toot",
+  "column.status": "Pouet",
   "column_back_button.label": "Retour",
   "column_header.hide_settings": "Masquer les paramètres",
   "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
   "column_header.moveRight_settings": "Déplacer la colonne vers la droite",
   "column_header.pin": "Épingler",
   "column_header.show_settings": "Afficher les paramètres",
-  "column_header.unpin": "Retirer",
+  "column_header.unpin": "Désépingler",
   "column_subheading.settings": "Paramètres",
   "community.column_settings.media_only": "Média uniquement",
   "compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé aux personnes mentionnées. Cependant, l’administration de votre instance et des instances réceptrices pourront inspecter ce message.",
-  "compose_form.direct_message_warning_learn_more": "En savoir plus",
-  "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.",
+  "compose_form.direct_message_warning_learn_more": "Plus d'informations",
+  "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par mot-clé car sa visibilité est réglée sur \"non listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par mot-clé.",
   "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
   "compose_form.placeholder": "Qu’avez-vous en tête ?",
@@ -87,7 +87,7 @@
   "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive.hide": "Marquer le média comme sensible",
   "compose_form.sensitive.marked": "Média marqué comme sensible",
-  "compose_form.sensitive.unmarked": "Média non marqué comme sensible",
+  "compose_form.sensitive.unmarked": "Le média n'est pas marqué comme sensible",
   "compose_form.spoiler.marked": "Le texte est caché derrière un avertissement",
   "compose_form.spoiler.unmarked": "Le texte n’est pas caché",
   "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
@@ -104,13 +104,17 @@
   "confirmations.logout.confirm": "Déconnexion",
   "confirmations.logout.message": "Êtes-vous sûr de vouloir vous déconnecter ?",
   "confirmations.mute.confirm": "Masquer",
-  "confirmations.mute.message": "Confirmez-vous le masquage de {name} ?",
+  "confirmations.mute.message": "Êtes-vous sûr·e de vouloir masquer {name} ?",
   "confirmations.redraft.confirm": "Effacer et ré-écrire",
   "confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer ce statut pour le ré-écrire ? Ses partages ainsi que ses mises en favori seront perdu·e·s et ses réponses seront orphelines.",
   "confirmations.reply.confirm": "Répondre",
   "confirmations.reply.message": "Répondre maintenant écrasera le message que vous êtes en train de composer. Voulez-vous vraiment continuer ?",
   "confirmations.unfollow.confirm": "Ne plus suivre",
   "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?",
+  "conversation.delete": "Supprimer la conversation",
+  "conversation.mark_as_read": "Marquer comme lu",
+  "conversation.open": "Afficher la conversation",
+  "conversation.with": "Avec {names}",
   "directory.federated": "De la fédiverse connue",
   "directory.local": "De {domain} seulement",
   "directory.new_arrivals": "Nouveaux arrivants",
@@ -147,7 +151,7 @@
   "empty_column.lists": "Vous n’avez pas encore de liste. Lorsque vous en créerez une, elle apparaîtra ici.",
   "empty_column.mutes": "Vous n’avez pas encore mis d'utilisateur·rice·s en silence.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres personnes pour débuter la conversation.",
-  "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes d’autres instances pour remplir le fil public",
+  "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes d’autres instances pour le remplir",
   "follow_request.authorize": "Accepter",
   "follow_request.reject": "Rejeter",
   "getting_started.developers": "Développeur·euse·s",
@@ -162,15 +166,15 @@
   "hashtag.column_header.tag_mode.any": "ou {additional}",
   "hashtag.column_header.tag_mode.none": "sans {additional}",
   "hashtag.column_settings.select.no_options_message": "Aucune suggestion trouvée",
-  "hashtag.column_settings.select.placeholder": "Ajouter des hashtags…",
+  "hashtag.column_settings.select.placeholder": "Ajouter des mots-clés…",
   "hashtag.column_settings.tag_mode.all": "Tous ces éléments",
   "hashtag.column_settings.tag_mode.any": "Au moins un de ces éléments",
   "hashtag.column_settings.tag_mode.none": "Aucun de ces éléments",
-  "hashtag.column_settings.tag_toggle": "Inclure des tags additionnels dans cette colonne",
-  "home.column_settings.basic": "Basique",
+  "hashtag.column_settings.tag_toggle": "Inclure des mots-clés additionnels dans cette colonne",
+  "home.column_settings.basic": "Base",
   "home.column_settings.show_reblogs": "Afficher les partages",
   "home.column_settings.show_replies": "Afficher les réponses",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Mettre à jour en temps réel",
   "intervals.full.days": "{number, plural, one {# jour} other {# jours}}",
   "intervals.full.hours": "{number, plural, one {# heure} other {# heures}}",
   "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@@ -195,7 +199,7 @@
   "keyboard_shortcuts.blocked": "pour ouvrir une liste d’utilisateur·rice·s bloqué·e·s",
   "keyboard_shortcuts.boost": "pour partager",
   "keyboard_shortcuts.column": "pour focaliser un statut dans l’une des colonnes",
-  "keyboard_shortcuts.compose": "pour centrer la zone de rédaction",
+  "keyboard_shortcuts.compose": "pour focaliser la zone de rédaction",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.direct": "pour ouvrir une colonne des messages directs",
   "keyboard_shortcuts.down": "pour descendre dans la liste",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Préférences",
   "navigation_bar.public_timeline": "Fil public global",
   "navigation_bar.security": "Sécurité",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "et {count, plural, one {# autre} other {# autres}}",
   "notification.favourite": "{name} a ajouté à ses favoris :",
   "notification.follow": "{name} vous suit",
   "notification.mention": "{name} vous a mentionné :",
@@ -295,6 +299,7 @@
   "poll.refresh": "Actualiser",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Voter",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Ajouter un sondage",
   "poll_button.remove_poll": "Supprimer le sondage",
   "privacy.change": "Ajuster la confidentialité du message",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 0967ef424..35639893e 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -1 +1,419 @@
-{}
+{
+  "account.add_or_remove_from_list": "Add or Remove from lists",
+  "account.badges.bot": "Bot",
+  "account.block": "Block @{name}",
+  "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
+  "account.direct": "Direct message @{name}",
+  "account.domain_blocked": "Domain hidden",
+  "account.edit_profile": "Edit profile",
+  "account.endorse": "Feature on profile",
+  "account.follow": "Follow",
+  "account.followers": "Followers",
+  "account.followers.empty": "No one follows this user yet.",
+  "account.follows": "Follows",
+  "account.follows.empty": "This user doesn't follow anyone yet.",
+  "account.follows_you": "Follows you",
+  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.last_status": "Last active",
+  "account.link_verified_on": "Ownership of this link was checked on {date}",
+  "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
+  "account.media": "Media",
+  "account.mention": "Mention @{name}",
+  "account.moved_to": "{name} has moved to:",
+  "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
+  "account.never_active": "Never",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots and replies",
+  "account.report": "Report @{name}",
+  "account.requested": "Awaiting approval",
+  "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
+  "account.unblock": "Unblock @{name}",
+  "account.unblock_domain": "Unhide {domain}",
+  "account.unendorse": "Don't feature on profile",
+  "account.unfollow": "Unfollow",
+  "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Rate limited",
+  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
+  "column.blocks": "Blocked users",
+  "column.community": "Local timeline",
+  "column.direct": "Direct messages",
+  "column.directory": "Browse profiles",
+  "column.domain_blocks": "Hidden domains",
+  "column.favourites": "Favourites",
+  "column.follow_requests": "Follow requests",
+  "column.home": "Home",
+  "column.lists": "Lists",
+  "column.mutes": "Muted users",
+  "column.notifications": "Notifications",
+  "column.pins": "Pinned toot",
+  "column.public": "Federated timeline",
+  "column.status": "Toot",
+  "column_back_button.label": "Back",
+  "column_header.hide_settings": "Hide settings",
+  "column_header.moveLeft_settings": "Move column to the left",
+  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.pin": "Pin",
+  "column_header.show_settings": "Show settings",
+  "column_header.unpin": "Unpin",
+  "column_subheading.settings": "Settings",
+  "community.column_settings.media_only": "Media only",
+  "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
+  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "compose_form.lock_disclaimer.lock": "locked",
+  "compose_form.placeholder": "What is on your mind?",
+  "compose_form.poll.add_option": "Add a choice",
+  "compose_form.poll.duration": "Poll duration",
+  "compose_form.poll.option_placeholder": "Choice {number}",
+  "compose_form.poll.remove_option": "Remove this choice",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive.hide": "Mark media as sensitive",
+  "compose_form.sensitive.marked": "Media is marked as sensitive",
+  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+  "compose_form.spoiler.marked": "Text is hidden behind warning",
+  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.spoiler_placeholder": "Write your warning here",
+  "confirmation_modal.cancel": "Cancel",
+  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.confirm": "Block",
+  "confirmations.block.message": "Are you sure you want to block {name}?",
+  "confirmations.delete.confirm": "Delete",
+  "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
+  "confirmations.logout.confirm": "Log out",
+  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.mute.confirm": "Mute",
+  "confirmations.mute.message": "Are you sure you want to mute {name}?",
+  "confirmations.redraft.confirm": "Delete & redraft",
+  "confirmations.redraft.message": "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.",
+  "confirmations.reply.confirm": "Reply",
+  "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
+  "confirmations.unfollow.confirm": "Unfollow",
+  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
+  "directory.federated": "From known fediverse",
+  "directory.local": "From {domain} only",
+  "directory.new_arrivals": "New arrivals",
+  "directory.recently_active": "Recently active",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
+  "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
+  "emoji_button.flags": "Flags",
+  "emoji_button.food": "Food & Drink",
+  "emoji_button.label": "Insert emoji",
+  "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objects",
+  "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
+  "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
+  "emoji_button.symbols": "Symbols",
+  "emoji_button.travel": "Travel & Places",
+  "empty_column.account_timeline": "No toots here!",
+  "empty_column.account_unavailable": "Profile unavailable",
+  "empty_column.blocks": "You haven't blocked any users yet.",
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.domain_blocks": "There are no hidden domains yet.",
+  "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.",
+  "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
+  "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+  "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+  "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
+  "empty_column.mutes": "You haven't muted any users yet.",
+  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
+  "follow_request.authorize": "Authorize",
+  "follow_request.reject": "Reject",
+  "getting_started.developers": "Developers",
+  "getting_started.directory": "Profile directory",
+  "getting_started.documentation": "Documentation",
+  "getting_started.heading": "Getting started",
+  "getting_started.invite": "Invite people",
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+  "getting_started.security": "Security",
+  "getting_started.terms": "Terms of service",
+  "hashtag.column_header.tag_mode.all": "and {additional}",
+  "hashtag.column_header.tag_mode.any": "or {additional}",
+  "hashtag.column_header.tag_mode.none": "without {additional}",
+  "hashtag.column_settings.select.no_options_message": "No suggestions found",
+  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.tag_mode.all": "All of these",
+  "hashtag.column_settings.tag_mode.any": "Any of these",
+  "hashtag.column_settings.tag_mode.none": "None of these",
+  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
+  "home.column_settings.basic": "Basic",
+  "home.column_settings.show_reblogs": "Show boosts",
+  "home.column_settings.show_replies": "Show replies",
+  "home.column_settings.update_live": "Update in real-time",
+  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
+  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
+  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
+  "introduction.federation.action": "Next",
+  "introduction.federation.federated.headline": "Federated",
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
+  "introduction.federation.home.headline": "Home",
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
+  "introduction.federation.local.headline": "Local",
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
+  "introduction.interactions.action": "Finish toot-orial!",
+  "introduction.interactions.favourite.headline": "Favourite",
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
+  "introduction.interactions.reblog.headline": "Boost",
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
+  "introduction.interactions.reply.headline": "Reply",
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
+  "introduction.welcome.action": "Let's go!",
+  "introduction.welcome.headline": "First steps",
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.blocked": "to open blocked users list",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.direct": "to open direct messages column",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourites": "to open favourites list",
+  "keyboard_shortcuts.federated": "to open federated timeline",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.home": "to open home timeline",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.local": "to open local timeline",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.muted": "to open muted users list",
+  "keyboard_shortcuts.my_profile": "to open your profile",
+  "keyboard_shortcuts.notifications": "to open notifications column",
+  "keyboard_shortcuts.pinned": "to open pinned toots list",
+  "keyboard_shortcuts.profile": "to open author's profile",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.requests": "to open follow requests list",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.start": "to open \"get started\" column",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
+  "lightbox.close": "Close",
+  "lightbox.next": "Next",
+  "lightbox.previous": "Previous",
+  "lightbox.view_context": "View context",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.edit.submit": "Change title",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "missing_indicator.sublabel": "This resource could not be found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "navigation_bar.apps": "Mobile apps",
+  "navigation_bar.blocks": "Blocked users",
+  "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.compose": "Compose new toot",
+  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.discover": "Discover",
+  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.edit_profile": "Edit profile",
+  "navigation_bar.favourites": "Favourites",
+  "navigation_bar.filters": "Muted words",
+  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.follows_and_followers": "Follows and followers",
+  "navigation_bar.info": "About this server",
+  "navigation_bar.keyboard_shortcuts": "Hotkeys",
+  "navigation_bar.lists": "Lists",
+  "navigation_bar.logout": "Logout",
+  "navigation_bar.mutes": "Muted users",
+  "navigation_bar.personal": "Personal",
+  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.preferences": "Preferences",
+  "navigation_bar.public_timeline": "Federated timeline",
+  "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.favourite": "{name} favourited your status",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.poll": "A poll you have voted in has ended",
+  "notification.reblog": "{name} boosted your status",
+  "notifications.clear": "Clear notifications",
+  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.column_settings.alert": "Desktop notifications",
+  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
+  "notifications.column_settings.filter_bar.show": "Show",
+  "notifications.column_settings.follow": "New followers:",
+  "notifications.column_settings.mention": "Mentions:",
+  "notifications.column_settings.poll": "Poll results:",
+  "notifications.column_settings.push": "Push notifications",
+  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Show in column",
+  "notifications.column_settings.sound": "Play sound",
+  "notifications.filter.all": "All",
+  "notifications.filter.boosts": "Boosts",
+  "notifications.filter.favourites": "Favourites",
+  "notifications.filter.follows": "Follows",
+  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.polls": "Poll results",
+  "notifications.group": "{count} notifications",
+  "poll.closed": "Closed",
+  "poll.refresh": "Refresh",
+  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
+  "poll_button.add_poll": "Add a poll",
+  "poll_button.remove_poll": "Remove poll",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {target}",
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Report {target}",
+  "search.placeholder": "Search",
+  "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "user",
+  "search_results.accounts": "People",
+  "search_results.hashtags": "Hashtags",
+  "search_results.statuses": "Toots",
+  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.copy": "Copy link to status",
+  "status.delete": "Delete",
+  "status.detailed_status": "Detailed conversation view",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "status.filtered": "Filtered",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.more": "More",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Mute conversation",
+  "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.read_more": "Read more",
+  "status.reblog": "Boost",
+  "status.reblog_private": "Boost to original audience",
+  "status.reblogged_by": "{name} boosted",
+  "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
+  "status.redraft": "Delete & re-draft",
+  "status.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
+  "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
+  "status.show_thread": "Show thread",
+  "status.uncached_media_warning": "Not available",
+  "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "suggestions.dismiss": "Dismiss suggestion",
+  "suggestions.header": "You might be interested in…",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "tabs_bar.search": "Search",
+  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
+  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.moments": "Moments remaining",
+  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.trending_now": "Trending now",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media ({formats})",
+  "upload_error.limit": "File upload limit exceeded.",
+  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.edit": "Edit",
+  "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 3cc44f43e..c5f670b91 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Respostando agora sobreescribirá a mensaxe que está a compoñer. Segura de querer proceder?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "Quere deixar de seguir a {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "Desde o fediverso coñecido",
   "directory.local": "Só desde {domain}",
   "directory.new_arrivals": "Novas achegas",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar repeticións",
   "home.column_settings.show_replies": "Mostrar respostas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualizar en tempo real",
   "intervals.full.days": "{number, plural,one {# día} other {# días}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Preferencias",
   "navigation_bar.public_timeline": "Liña temporal federada",
   "navigation_bar.security": "Seguridade",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "e {count, plural, one {# outro} other {# outros}}",
   "notification.favourite": "{name} marcou como favorito o seu estado",
   "notification.follow": "{name} está a seguila",
   "notification.mention": "{name} mencionoute",
@@ -295,6 +299,7 @@
   "poll.refresh": "Actualizar",
   "poll.total_votes": "{count, plural, one {# voto} outros {# votos}}",
   "poll.vote": "Votar",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Engadir sondaxe",
   "poll_button.remove_poll": "Eliminar sondaxe",
   "privacy.change": "Axustar a intimidade do estado",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index b6cc3e6ce..80a072dcc 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "להפסיק מעקב",
   "confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "שינוי פרטיות ההודעה",
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index e2d1eb49d..18e68ce7c 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 6daabc694..dcfb92df5 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Podesi status privatnosti",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index f5a02065b..3dd88c078 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -4,7 +4,7 @@
   "account.block": "@{name} letiltása",
   "account.block_domain": "Minden elrejtése innen: {domain}",
   "account.blocked": "Letiltva",
-  "account.cancel_follow_request": "Cancel follow request",
+  "account.cancel_follow_request": "Követési kérelem törlése",
   "account.direct": "Közvetlen üzenet @{name} számára",
   "account.domain_blocked": "Rejtett domain",
   "account.edit_profile": "Profil szerkesztése",
@@ -16,7 +16,7 @@
   "account.follows.empty": "Ez a felhasználó még senkit sem követ.",
   "account.follows_you": "Követ téged",
   "account.hide_reblogs": "@{name} megtolásainak némítása",
-  "account.last_status": "Last active",
+  "account.last_status": "Utoljára aktív",
   "account.link_verified_on": "A linket ellenőriztük: {date}",
   "account.locked_info": "Ez a fiók zárt. A tulaj engedélyezi, ki követheti őt.",
   "account.media": "Média",
@@ -25,7 +25,7 @@
   "account.mute": "@{name} némítása",
   "account.mute_notifications": "@{name} értesítéseinek némítása",
   "account.muted": "Némítva",
-  "account.never_active": "Never",
+  "account.never_active": "Soha",
   "account.posts": "Tülkölés",
   "account.posts_with_replies": "Tülkölés válaszokkal",
   "account.report": "@{name} jelentése",
@@ -38,11 +38,11 @@
   "account.unfollow": "Követés vége",
   "account.unmute": "@{name} némítás feloldása",
   "account.unmute_notifications": "@{name} némított értesítéseinek feloldása",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "Kérlek, próbáld újra {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Forgalomkorlátozás",
   "alert.unexpected.message": "Váratlan hiba történt.",
   "alert.unexpected.title": "Hoppá!",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "autosuggest_hashtag.per_week": "{count}/hét",
   "boost_modal.combo": "Hogy átugord ezt következő alkalommal, használd {combo}",
   "bundle_column_error.body": "Hiba történt a komponens betöltése közben.",
   "bundle_column_error.retry": "Próbáld újra",
@@ -53,7 +53,7 @@
   "column.blocks": "Letiltott felhasználók",
   "column.community": "Helyi idővonal",
   "column.direct": "Közvetlen üzenetek",
-  "column.directory": "Browse profiles",
+  "column.directory": "Profilok böngészése",
   "column.domain_blocks": "Rejtett domainek",
   "column.favourites": "Kedvencek",
   "column.follow_requests": "Követési kérelmek",
@@ -63,7 +63,7 @@
   "column.notifications": "Értesítések",
   "column.pins": "Kitűzött tülkök",
   "column.public": "Nyilvános idővonal",
-  "column.status": "Toot",
+  "column.status": "Tülk",
   "column_back_button.label": "Vissza",
   "column_header.hide_settings": "Beállítások elrejtése",
   "column_header.moveLeft_settings": "Oszlop elmozdítása balra",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "Biztos, hogy véglegesen törölni szeretnéd ezt a listát?",
   "confirmations.domain_block.confirm": "Teljes domain elrejtése",
   "confirmations.domain_block.message": "Egészen biztos, hogy le szeretnéd tiltani a teljes {domain}-t? A legtöbb esetben néhány célzott tiltás vagy némítás elegendő és kívánatosabb megoldás. Semmilyen tartalmat nem fogsz látni ebből a domainből se idővonalakon, se értesítésekben. Az ebben a domainben lévő követőidet is eltávolítjuk.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Kijelentkezés",
+  "confirmations.logout.message": "Biztosan ki akar jelentkezni?",
   "confirmations.mute.confirm": "Némítás",
   "confirmations.mute.message": "Biztos, hogy némítani szeretnéd {name}?",
   "confirmations.redraft.confirm": "Törlés és újraírás",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Ha most válaszolsz, ez felülírja a most szerkesztés alatt álló üzenetet. Mégis ezt szeretnéd?",
   "confirmations.unfollow.confirm": "Követés visszavonása",
   "confirmations.unfollow.message": "Biztos, hogy vissza szeretnéd vonni {name} követését?",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "Beszélgetés törlése",
+  "conversation.mark_as_read": "Megjelölés olvasottként",
+  "conversation.open": "Beszélgetés megtekintése",
+  "conversation.with": "{names}-el/al",
+  "directory.federated": "Az ismert fediverzumból",
+  "directory.local": "Csak {domain}-ból/ből",
+  "directory.new_arrivals": "Új csatlakozók",
+  "directory.recently_active": "Nemrég aktív",
   "embed.instructions": "Ágyazd be ezt a tülköt a weboldaladba az alábbi kód kimásolásával.",
   "embed.preview": "Így fog kinézni:",
   "emoji_button.activity": "Aktivitás",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Alapértelmezések",
   "home.column_settings.show_reblogs": "Megtolások mutatása",
   "home.column_settings.show_replies": "Válaszok mutatása",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Frissítés valós időben",
   "intervals.full.days": "{number, plural, one {# nap} other {# nap}}",
   "intervals.full.hours": "{number, plural, one {# óra} other {# óra}}",
   "intervals.full.minutes": "{number, plural, one {# perc} other {# perc}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Beállítások",
   "navigation_bar.public_timeline": "Föderációs idővonal",
   "navigation_bar.security": "Biztonság",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "és {count, plural, one {# másik} other {# másik}}",
   "notification.favourite": "{name} kedvencnek jelölte egy tülködet",
   "notification.follow": "{name} követ téged",
   "notification.mention": "{name} megemlített",
@@ -295,6 +299,7 @@
   "poll.refresh": "Frissítés",
   "poll.total_votes": "{count, plural, one {# szavazat} other {# szavazat}}",
   "poll.vote": "Szavazás",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Új szavazás",
   "poll_button.remove_poll": "Szavazás törlése",
   "privacy.change": "Tülk láthatóságának módosítása",
@@ -369,7 +374,7 @@
   "status.show_more": "Többet",
   "status.show_more_all": "Többet mindenhol",
   "status.show_thread": "Szál mutatása",
-  "status.uncached_media_warning": "Not available",
+  "status.uncached_media_warning": "Nem elérhető",
   "status.unmute_conversation": "Beszélgetés némításának kikapcsolása",
   "status.unpin": "Kitűzés eltávolítása a profilodról",
   "suggestions.dismiss": "Javaslat elvetése",
@@ -385,22 +390,22 @@
   "time_remaining.moments": "Pillanatok vannak hátra",
   "time_remaining.seconds": "{number, plural, one {# másodperc} other {# másodperc}} van hátra",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {résztvevő} other {résztvevő}} beszélget",
-  "trends.trending_now": "Trending now",
+  "trends.trending_now": "Most trendi",
   "ui.beforeunload": "A piszkozatod el fog veszni, ha elhagyod a Mastodon-t.",
   "upload_area.title": "Húzd ide a feltöltéshez",
   "upload_button.label": "Média hozzáadása (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Túllépted a fájl feltöltési limitet.",
   "upload_error.poll": "Szavazásnál nem lehet fájlt feltölteni.",
   "upload_form.description": "Leírás látáskorlátozottak számára",
-  "upload_form.edit": "Edit",
+  "upload_form.edit": "Szerkesztés",
   "upload_form.undo": "Mégsem",
-  "upload_modal.analyzing_picture": "Analyzing picture…",
-  "upload_modal.apply": "Apply",
-  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
-  "upload_modal.detect_text": "Detect text from picture",
-  "upload_modal.edit_media": "Edit media",
-  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
-  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_modal.analyzing_picture": "Kép elemzése…",
+  "upload_modal.apply": "Alkalmazás",
+  "upload_modal.description_placeholder": "A gyors, barna róka átugrik a lusta kutya fölött",
+  "upload_modal.detect_text": "Szöveg felismerése a képről",
+  "upload_modal.edit_media": "Média szerkesztése",
+  "upload_modal.hint": "Kattints vagy húzd a kört az előnézetben arra a fókuszpontra, mely minden megjelenített bélyegképen látható kell, legyen.",
+  "upload_modal.preview_label": "Előnézet ({ratio})",
   "upload_progress.label": "Feltöltés...",
   "video.close": "Videó bezárása",
   "video.exit_fullscreen": "Kilépés teljes képernyőből",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 1484c76df..54077dc05 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Ապահետեւել",
   "confirmations.unfollow.message": "Վստա՞հ ես, որ ուզում ես այլեւս չհետեւել {name}֊ին։",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Կարգավորել թթի գաղտնիությունը",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index c9e48a1a6..21fe401b8 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Membalas sekarang akan menimpa pesan yang sedang Anda buat. Anda yakin ingin melanjutkan?",
   "confirmations.unfollow.confirm": "Berhenti mengikuti",
   "confirmations.unfollow.message": "Apakah anda ingin berhenti mengikuti {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Tentukan privasi status",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 6c1b7fa8b..092b163fb 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Aranjar privateso di mesaji",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index dc43bcb5c..1f2f6e0e6 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -16,7 +16,7 @@
   "account.follows.empty": "Questo utente non segue ancora nessuno.",
   "account.follows_you": "Ti segue",
   "account.hide_reblogs": "Nascondi condivisioni da @{name}",
-  "account.last_status": "Last active",
+  "account.last_status": "Ultima attività",
   "account.link_verified_on": "La proprietà di questo link è stata controllata il {date}",
   "account.locked_info": "Il livello di privacy di questo account è impostato a \"bloccato\". Il proprietario esamina manualmente le richieste di seguirlo.",
   "account.media": "Media",
@@ -25,7 +25,7 @@
   "account.mute": "Silenzia @{name}",
   "account.mute_notifications": "Silenzia notifiche da @{name}",
   "account.muted": "Silenziato",
-  "account.never_active": "Never",
+  "account.never_active": "Mai",
   "account.posts": "Toot",
   "account.posts_with_replies": "Toot e risposte",
   "account.report": "Segnala @{name}",
@@ -53,7 +53,7 @@
   "column.blocks": "Utenti bloccati",
   "column.community": "Timeline locale",
   "column.direct": "Messaggi diretti",
-  "column.directory": "Browse profiles",
+  "column.directory": "Sfoglia profili",
   "column.domain_blocks": "Domini nascosti",
   "column.favourites": "Apprezzati",
   "column.follow_requests": "Richieste di amicizia",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "Sei sicuro di voler cancellare definitivamente questa lista?",
   "confirmations.domain_block.confirm": "Nascondi intero dominio",
   "confirmations.domain_block.message": "Sei davvero sicuro che vuoi bloccare l'intero {domain}? Nella maggior parte dei casi, pochi blocchi o silenziamenti mirati sono sufficienti e preferibili. Non vedrai nessun contenuto di quel dominio né nelle timeline pubbliche né nelle notifiche. I tuoi seguaci di quel dominio saranno eliminati.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Esci",
+  "confirmations.logout.message": "Sei sicuro di voler uscire?",
   "confirmations.mute.confirm": "Silenzia",
   "confirmations.mute.message": "Sei sicuro di voler silenziare {name}?",
   "confirmations.redraft.confirm": "Cancella e riscrivi",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Se rispondi ora, il messaggio che stai componendo sarà sovrascritto. Sei sicuro di voler continuare?",
   "confirmations.unfollow.confirm": "Smetti di seguire",
   "confirmations.unfollow.message": "Sei sicuro che non vuoi più seguire {name}?",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "Elimina conversazione",
+  "conversation.mark_as_read": "Segna come letto",
+  "conversation.open": "Visualizza conversazione",
+  "conversation.with": "Con {names}",
+  "directory.federated": "Da un fediverso noto",
+  "directory.local": "Solo da {domain}",
+  "directory.new_arrivals": "Nuovi arrivi",
+  "directory.recently_active": "Attivo di recente",
   "embed.instructions": "Inserisci questo status nel tuo sito copiando il codice qui sotto.",
   "embed.preview": "Ecco come apparirà:",
   "emoji_button.activity": "Attività",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Semplice",
   "home.column_settings.show_reblogs": "Mostra post condivisi",
   "home.column_settings.show_replies": "Mostra risposte",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Aggiorna in tempo reale",
   "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}",
   "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Impostazioni",
   "navigation_bar.public_timeline": "Timeline federata",
   "navigation_bar.security": "Sicurezza",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "e {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} ha apprezzato il tuo post",
   "notification.follow": "{name} ha iniziato a seguirti",
   "notification.mention": "{name} ti ha menzionato",
@@ -295,6 +299,7 @@
   "poll.refresh": "Aggiorna",
   "poll.total_votes": "{count, plural, one {# voto} other {# voti}}",
   "poll.vote": "Vota",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Aggiungi un sondaggio",
   "poll_button.remove_poll": "Rimuovi sondaggio",
   "privacy.change": "Modifica privacy del post",
@@ -369,7 +374,7 @@
   "status.show_more": "Mostra di più",
   "status.show_more_all": "Mostra di più per tutti",
   "status.show_thread": "Mostra thread",
-  "status.uncached_media_warning": "Not available",
+  "status.uncached_media_warning": "Non disponibile",
   "status.unmute_conversation": "Annulla silenzia conversazione",
   "status.unpin": "Non fissare in cima al profilo",
   "suggestions.dismiss": "Elimina suggerimento",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 64baed71d..d59b2c7f2 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -38,7 +38,7 @@
   "account.unfollow": "フォロー解除",
   "account.unmute": "@{name}さんのミュートを解除",
   "account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
-  "alert.rate_limited.message": "{retry_time, time, medium} 後に再試行してください。",
+  "alert.rate_limited.message": "{retry_time, time, medium} 以降に再試行してください。",
   "alert.rate_limited.title": "制限に達しました",
   "alert.unexpected.message": "不明なエラーが発生しました。",
   "alert.unexpected.title": "エラー!",
@@ -53,7 +53,7 @@
   "column.blocks": "ブロックしたユーザー",
   "column.community": "ローカルタイムライン",
   "column.direct": "ダイレクトメッセージ",
-  "column.directory": "Browse profiles",
+  "column.directory": "ディレクトリ",
   "column.domain_blocks": "非表示にしたドメイン",
   "column.favourites": "お気に入り",
   "column.follow_requests": "フォローリクエスト",
@@ -63,7 +63,7 @@
   "column.notifications": "通知",
   "column.pins": "固定されたトゥート",
   "column.public": "連合タイムライン",
-  "column.status": "Toot",
+  "column.status": "トゥート",
   "column_back_button.label": "戻る",
   "column_header.hide_settings": "設定を隠す",
   "column_header.moveLeft_settings": "カラムを左に移動する",
@@ -115,7 +115,11 @@
   "confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
   "confirmations.unfollow.confirm": "フォロー解除",
   "confirmations.unfollow.message": "本当に{name}さんのフォローを解除しますか?",
-  "directory.federated": "既知の連合全体",
+  "conversation.delete": "このやりとりを削除",
+  "conversation.mark_as_read": "既読にする",
+  "conversation.open": "会話を表示",
+  "conversation.with": "{names}",
+  "directory.federated": "既知の連合より",
   "directory.local": "{domain} のみ",
   "directory.new_arrivals": "新着順",
   "directory.recently_active": "最近の活動順",
@@ -174,7 +178,7 @@
   "home.column_settings.basic": "基本設定",
   "home.column_settings.show_reblogs": "ブースト表示",
   "home.column_settings.show_replies": "返信表示",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "リアルタイムで更新",
   "intervals.full.days": "{number}日",
   "intervals.full.hours": "{number}時間",
   "intervals.full.minutes": "{number}分",
@@ -269,7 +273,7 @@
   "navigation_bar.public_timeline": "連合タイムライン",
   "navigation_bar.misc": "その他",
   "navigation_bar.security": "セキュリティ",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "と、他 {count} 件",
   "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
   "notification.follow": "{name}さんにフォローされました",
   "notification.mention": "{name}さんがあなたに返信しました",
@@ -300,6 +304,7 @@
   "poll.refresh": "更新",
   "poll.total_votes": "{count}票",
   "poll.vote": "投票",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "アンケートを追加",
   "poll_button.remove_poll": "アンケートを削除",
   "privacy.change": "公開範囲を変更",
@@ -390,7 +395,7 @@
   "time_remaining.moments": "まもなく終了",
   "time_remaining.seconds": "残り{number}秒",
   "trends.count_by_accounts": "{count}人がトゥート",
-  "trends.trending_now": "トレンドタグ",
+  "trends.trending_now": "トレンド",
   "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
   "upload_area.title": "ドラッグ&ドロップでアップロード",
   "upload_button.label": "メディアを追加 ({formats})",
@@ -404,7 +409,7 @@
   "upload_modal.description_placeholder": "素早い茶色の狐はのろまな犬を飛び越える",
   "upload_modal.detect_text": "画像からテキストを検出",
   "upload_modal.edit_media": "メディアを編集",
-  "upload_modal.hint": "画像をクリックするか円をドラッグすると全てのサムネイルで注目する場所を選ぶことができます",
+  "upload_modal.hint": "画像をクリックするか円をドラッグすると全てのサムネイルの中心点を決めることができます",
   "upload_modal.preview_label": "プレビュー ({ratio})",
   "upload_progress.label": "アップロード中...",
   "video.close": "動画を閉じる",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index e2a6ee6c6..51b7deb5e 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "ნუღარ მიჰყვები",
   "confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "სტატუსის კონფიდენციალურობის მითითება",
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index a07302f0a..13f2d23d2 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Жауабыңыз жазып жатқан жазбаңыздың үстіне кетеді. Жалғастырамыз ба?",
   "confirmations.unfollow.confirm": "Оқымау",
   "confirmations.unfollow.message": "\"{name} атты қолданушыға енді жазылғыңыз келмей ме?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Жаңарту",
   "poll.total_votes": "{count, plural, one {# дауыс} other {# дауыс}}",
   "poll.vote": "Дауыс беру",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Сауалнама қосу",
   "poll_button.remove_poll": "Сауалнаманы өшіру",
   "privacy.change": "Құпиялылықты реттеу",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 3ec9a8a16..60a197126 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -63,7 +63,7 @@
   "column.notifications": "알림",
   "column.pins": "고정된 툿",
   "column.public": "연합 타임라인",
-  "column.status": "Toot",
+  "column.status": "툿",
   "column_back_button.label": "돌아가기",
   "column_header.hide_settings": "설정 숨기기",
   "column_header.moveLeft_settings": "왼쪽으로 이동",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "답글을 달기 위해 현재 작성 중인 메시지가 덮어 씌워집니다. 진행하시겠습니까?",
   "confirmations.unfollow.confirm": "언팔로우",
   "confirmations.unfollow.message": "정말로 {name}를 언팔로우하시겠습니까?",
+  "conversation.delete": "대화 삭제",
+  "conversation.mark_as_read": "읽은 상태로 표시",
+  "conversation.open": "대화 보기",
+  "conversation.with": "{names} 와 함께",
   "directory.federated": "알려진 연합우주로부터",
   "directory.local": "{domain}에서만",
   "directory.new_arrivals": "새로운 사람들",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "기본 설정",
   "home.column_settings.show_reblogs": "부스트 표시",
   "home.column_settings.show_replies": "답글 표시",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "실시간 갱신",
   "intervals.full.days": "{number} 일",
   "intervals.full.hours": "{number} 시간",
   "intervals.full.minutes": "{number} 분",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "사용자 설정",
   "navigation_bar.public_timeline": "연합 타임라인",
   "navigation_bar.security": "보안",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "그리고 {count}개의 기타 항목",
   "notification.favourite": "{name}님이 즐겨찾기 했습니다",
   "notification.follow": "{name}님이 나를 팔로우 했습니다",
   "notification.mention": "{name}님이 답글을 보냈습니다",
@@ -295,6 +299,7 @@
   "poll.refresh": "새로고침",
   "poll.total_votes": "{count} 표",
   "poll.vote": "투표",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "투표 추가",
   "poll_button.remove_poll": "투표 삭제",
   "privacy.change": "포스트의 프라이버시 설정을 변경",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 2de037f16..35639893e 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 8d281c9d5..b837cc42b 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Atbildot tagad tava ziņa ko šobrīd raksti tiks pārrakstīta. Vai tiešām vēlies turpināt?",
   "confirmations.unfollow.confirm": "Nesekot",
   "confirmations.unfollow.message": "Vai tiešam vairs nevēlies sekot lietotājam {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index 9bd5eef72..da7ddfc80 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 73e7b3905..edba37434 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -16,7 +16,7 @@
   "account.follows.empty": "Deze gebruiker volgt nog niemand.",
   "account.follows_you": "Volgt jou",
   "account.hide_reblogs": "Verberg boosts van @{name}",
-  "account.last_status": "Last active",
+  "account.last_status": "Laatst actief",
   "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}",
   "account.locked_info": "De privacystatus van dit account is op besloten gezet. De eigenaar bepaalt handmatig wie hen kan volgen.",
   "account.media": "Media",
@@ -25,7 +25,7 @@
   "account.mute": "Negeer @{name}",
   "account.mute_notifications": "Negeer meldingen van @{name}",
   "account.muted": "Genegeerd",
-  "account.never_active": "Never",
+  "account.never_active": "Nooit",
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots en reacties",
   "account.report": "Rapporteer @{name}",
@@ -38,8 +38,8 @@
   "account.unfollow": "Ontvolgen",
   "account.unmute": "@{name} niet langer negeren",
   "account.unmute_notifications": "@{name} meldingen niet langer negeren",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "Probeer het nog een keer na {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Beperkt te gebruiken",
   "alert.unexpected.message": "Er deed zich een onverwachte fout voor",
   "alert.unexpected.title": "Oeps!",
   "autosuggest_hashtag.per_week": "{count} per week",
@@ -53,7 +53,7 @@
   "column.blocks": "Geblokkeerde gebruikers",
   "column.community": "Lokale tijdlijn",
   "column.direct": "Directe berichten",
-  "column.directory": "Browse profiles",
+  "column.directory": "Gebruikersgids",
   "column.domain_blocks": "Genegeerde servers",
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "Weet je zeker dat je deze lijst definitief wilt verwijderen?",
   "confirmations.domain_block.confirm": "Verberg alles van deze server",
   "confirmations.domain_block.message": "Weet je het echt heel erg zeker dat je alles van {domain} wilt negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en beter. Je zult geen toots van deze server op openbare tijdlijnen zien of in jouw meldingen. Jouw volgers van deze server worden verwijderd.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Uitloggen",
+  "confirmations.logout.message": "Weet je zeker dat je wilt uitloggen?",
   "confirmations.mute.confirm": "Negeren",
   "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
   "confirmations.redraft.confirm": "Verwijderen en herschrijven",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Door nu te reageren overschrijf je de toot die je op dit moment aan het schrijven bent. Weet je zeker dat je verder wil gaan?",
   "confirmations.unfollow.confirm": "Ontvolgen",
   "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "Gesprek verwijderen",
+  "conversation.mark_as_read": "Als gelezen markeren",
+  "conversation.open": "Gesprek tonen",
+  "conversation.with": "Met {names}",
+  "directory.federated": "Fediverse (wat bekend is)",
+  "directory.local": "Alleen {domain}",
+  "directory.new_arrivals": "Nieuwe accounts",
+  "directory.recently_active": "Onlangs actief",
   "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.",
   "embed.preview": "Zo komt het eruit te zien:",
   "emoji_button.activity": "Activiteiten",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Algemeen",
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "In realtime bijwerken",
   "intervals.full.days": "{number, plural, one {# dag} other {# dagen}}",
   "intervals.full.hours": "{number, plural, one {# uur} other {# uur}}",
   "intervals.full.minutes": "{number, plural, one {# minuut} other {# minuten}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Instellingen",
   "navigation_bar.public_timeline": "Globale tijdlijn",
   "navigation_bar.security": "Beveiliging",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "en {count, plural, one {# meer} other {# meer}}",
   "notification.favourite": "{name} voegde jouw toot als favoriet toe",
   "notification.follow": "{name} volgt jou nu",
   "notification.mention": "{name} vermeldde jou",
@@ -295,6 +299,7 @@
   "poll.refresh": "Vernieuwen",
   "poll.total_votes": "{count, plural, one {# stem} other {# stemmen}}",
   "poll.vote": "Stemmen",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Poll toevoegen",
   "poll_button.remove_poll": "Poll verwijderen",
   "privacy.change": "Zichtbaarheid toot aanpassen",
@@ -369,7 +374,7 @@
   "status.show_more": "Meer tonen",
   "status.show_more_all": "Alles meer tonen",
   "status.show_thread": "Gesprek tonen",
-  "status.uncached_media_warning": "Not available",
+  "status.uncached_media_warning": "Niet beschikbaar",
   "status.unmute_conversation": "Gesprek niet langer negeren",
   "status.unpin": "Van profielpagina losmaken",
   "suggestions.dismiss": "Voorstel verwerpen",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index dda402494..c4003193e 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -77,11 +77,11 @@
   "compose_form.direct_message_warning_learn_more": "Lær meir",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
-  "compose_form.lock_disclaimer.lock": "locked",
-  "compose_form.placeholder": "What is on your mind?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
+  "compose_form.lock_disclaimer.lock": "låst",
+  "compose_form.placeholder": "Kva har du på hjartet?",
+  "compose_form.poll.add_option": "Legg til eit punkt",
+  "compose_form.poll.duration": "Varigheit for spørring",
+  "compose_form.poll.option_placeholder": "Val {number}",
   "compose_form.poll.remove_option": "Remove this choice",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 8fc722037..8fe7c9cfd 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Slutt å følge",
   "confirmations.unfollow.message": "Er du sikker på at du vil slutte å følge {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Justér synlighet",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index d5abe89fb..550057f23 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -4,7 +4,7 @@
   "account.block": "Blocar @{name}",
   "account.block_domain": "Tot amagar del domeni {domain}",
   "account.blocked": "Blocat",
-  "account.cancel_follow_request": "Cancel follow request",
+  "account.cancel_follow_request": "Anullar la demanda de seguiment",
   "account.direct": "Escriure un MP a @{name}",
   "account.domain_blocked": "Domeni amagat",
   "account.edit_profile": "Modificar lo perfil",
@@ -16,7 +16,7 @@
   "account.follows.empty": "Aqueste utilizaire sèc pas degun pel moment.",
   "account.follows_you": "Vos sèc",
   "account.hide_reblogs": "Rescondre los partatges de @{name}",
-  "account.last_status": "Last active",
+  "account.last_status": "Darrièra activitat",
   "account.link_verified_on": "La proprietat d’aqueste ligam foguèt verificada lo {date}",
   "account.locked_info": "L’estatut de privacitat del compte es configurat sus clavat. Lo proprietari causís qual pòt sègre son compte.",
   "account.media": "Mèdias",
@@ -25,7 +25,7 @@
   "account.mute": "Rescondre @{name}",
   "account.mute_notifications": "Rescondre las notificacions de @{name}",
   "account.muted": "Mes en silenci",
-  "account.never_active": "Never",
+  "account.never_active": "Jamai",
   "account.posts": "Tuts",
   "account.posts_with_replies": "Tuts e responsas",
   "account.report": "Senhalar @{name}",
@@ -38,11 +38,11 @@
   "account.unfollow": "Quitar de sègre",
   "account.unmute": "Quitar de rescondre @{name}",
   "account.unmute_notifications": "Mostrar las notificacions de @{name}",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "Mercés de tornar ensajar aprèp {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Taus limitat",
   "alert.unexpected.message": "Una error s’es producha.",
   "alert.unexpected.title": "Ops !",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "autosuggest_hashtag.per_week": "{count} per setmana",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
   "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
   "bundle_column_error.retry": "Tornar ensajar",
@@ -53,7 +53,7 @@
   "column.blocks": "Personas blocadas",
   "column.community": "Flux public local",
   "column.direct": "Messatges dirèctes",
-  "column.directory": "Browse profiles",
+  "column.directory": "Percórrer los perfils",
   "column.domain_blocks": "Domenis resconduts",
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
@@ -63,7 +63,7 @@
   "column.notifications": "Notificacions",
   "column.pins": "Tuts penjats",
   "column.public": "Flux public global",
-  "column.status": "Toot",
+  "column.status": "Tut",
   "column_back_button.label": "Tornar",
   "column_header.hide_settings": "Amagar los paramètres",
   "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "Volètz vertadièrament suprimir aquesta lista per totjorn ?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
   "confirmations.domain_block.message": "Volètz vertadièrament blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.\nVeiretz pas cap de contengut d’aquel domeni dins cap de flux public o dins vòstras notificacions. Vòstres seguidors d’aquel domeni seràn levats.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Desconnexion",
+  "confirmations.logout.message": "Volètz vertadièrament vos desconnectar ?",
   "confirmations.mute.confirm": "Rescondre",
   "confirmations.mute.message": "Volètz vertadièrament rescondre {name} ?",
   "confirmations.redraft.confirm": "Escafar & tornar formular",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Respondre remplaçarà lo messatge que sètz a escriure. Volètz vertadièrament contunhar ?",
   "confirmations.unfollow.confirm": "Quitar de sègre",
   "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "Suprimir la conversacion",
+  "conversation.mark_as_read": "Marcar coma legida",
+  "conversation.open": "Veire la conversacion",
+  "conversation.with": "Amb {names}",
+  "directory.federated": "Del fediverse conegut",
+  "directory.local": "Solament de {domain}",
+  "directory.new_arrivals": "Nòus-venguts",
+  "directory.recently_active": "Actius fa res",
   "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
   "embed.preview": "Semblarà aquò :",
   "emoji_button.activity": "Activitats",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Basic",
   "home.column_settings.show_reblogs": "Mostrar los partatges",
   "home.column_settings.show_replies": "Mostrar las responsas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualizacion en dirècte",
   "intervals.full.days": "{number, plural, one {# jorn} other {# jorns}}",
   "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} other {# minutas}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Preferéncias",
   "navigation_bar.public_timeline": "Flux public global",
   "navigation_bar.security": "Seguretat",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "e {count, plural, un {# autre} other {# autres}}",
   "notification.favourite": "{name} a ajustat a sos favorits",
   "notification.follow": "{name} vos sèc",
   "notification.mention": "{name} vos a mencionat",
@@ -295,6 +299,7 @@
   "poll.refresh": "Actualizar",
   "poll.total_votes": "{count, plural, one {# vòte} other {# vòtes}}",
   "poll.vote": "Votar",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Ajustar un sondatge",
   "poll_button.remove_poll": "Levar lo sondatge",
   "privacy.change": "Ajustar la confidencialitat del messatge",
@@ -369,7 +374,7 @@
   "status.show_more": "Desplegar",
   "status.show_more_all": "Los desplegar totes",
   "status.show_thread": "Mostrar lo fil",
-  "status.uncached_media_warning": "Not available",
+  "status.uncached_media_warning": "Pas disponible",
   "status.unmute_conversation": "Tornar mostrar la conversacion",
   "status.unpin": "Tirar del perfil",
   "suggestions.dismiss": "Regetar la suggestion",
@@ -385,22 +390,22 @@
   "time_remaining.moments": "Moments restants",
   "time_remaining.seconds": "demòra{number, plural, one { # segonda} other {n # segondas}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} ne charra other {people}} ne charran",
-  "trends.trending_now": "Trending now",
+  "trends.trending_now": "Tendéncia del moment",
   "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Talha maximum pels mandadís subrepassada.",
   "upload_error.poll": "Lo mandadís de fichièr es pas autorizat pels sondatges.",
   "upload_form.description": "Descripcion pels mal vesents",
-  "upload_form.edit": "Edit",
+  "upload_form.edit": "Modificar",
   "upload_form.undo": "Suprimir",
-  "upload_modal.analyzing_picture": "Analyzing picture…",
-  "upload_modal.apply": "Apply",
-  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
-  "upload_modal.detect_text": "Detect text from picture",
-  "upload_modal.edit_media": "Edit media",
-  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
-  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_modal.analyzing_picture": "Analisi de l’imatge…",
+  "upload_modal.apply": "Aplicar",
+  "upload_modal.description_placeholder": "Lo dròlle bilingüe manja un yaourt de ròcs exagonals e kiwis verds farà un an mai",
+  "upload_modal.detect_text": "Detectar lo tèxt de l’imatge",
+  "upload_modal.edit_media": "Modificar lo mèdia",
+  "upload_modal.hint": "Clicatz o lissatz lo cercle de l’apercebut per causir lo ponch que serà totjorn visible dins las vinhetas.",
+  "upload_modal.preview_label": "Apercebut ({ratio})",
   "upload_progress.label": "Mandadís…",
   "video.close": "Tampar la vidèo",
   "video.exit_fullscreen": "Sortir plen ecran",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 395f3927a..b5af4a49a 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -105,8 +105,8 @@
   "confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?",
   "confirmations.domain_block.confirm": "Ukryj wszysyko z domeny",
   "confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Wyloguj",
+  "confirmations.logout.message": "Czy na pewno chcesz się wylogować?",
   "confirmations.mute.confirm": "Wycisz",
   "confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
   "confirmations.redraft.confirm": "Usuń i przeredaguj",
@@ -115,6 +115,10 @@
   "confirmations.reply.message": "W ten sposób utracisz wpis który obecnie tworzysz. Czy na pewno chcesz to zrobić?",
   "confirmations.unfollow.confirm": "Przestań śledzić",
   "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?",
+  "conversation.delete": "Usuń rozmowę",
+  "conversation.mark_as_read": "Oznacz jako przeczytane",
+  "conversation.open": "Zobacz rozmowę",
+  "conversation.with": "Z {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -300,6 +304,7 @@
   "poll.refresh": "Odśwież",
   "poll.total_votes": "{count, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}",
   "poll.vote": "Zagłosuj",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Dodaj głosowanie",
   "poll_button.remove_poll": "Usuń głosowanie",
   "privacy.change": "Dostosuj widoczność wpisów",
@@ -397,7 +402,7 @@
   "upload_error.limit": "Przekroczono limit plików do wysłania.",
   "upload_error.poll": "Dołączanie plików nie dozwolone z głosowaniami.",
   "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
-  "upload_form.edit": "Edit",
+  "upload_form.edit": "Edytuj",
   "upload_form.undo": "Usuń",
   "upload_modal.analyzing_picture": "Analyzing picture…",
   "upload_modal.apply": "Zastosuj",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index debf9e6f6..4bc703206 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -4,7 +4,7 @@
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo de {domain}",
   "account.blocked": "Bloqueado",
-  "account.cancel_follow_request": "Cancel follow request",
+  "account.cancel_follow_request": "Cancelar solicitação para seguir",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domínio escondido",
   "account.edit_profile": "Editar perfil",
@@ -16,7 +16,7 @@
   "account.follows.empty": "Esse usuário não segue ninguém no momento.",
   "account.follows_you": "Segue você",
   "account.hide_reblogs": "Esconder compartilhamentos de @{name}",
-  "account.last_status": "Last active",
+  "account.last_status": "Última atividade",
   "account.link_verified_on": "A posse desse link foi verificada em {date}",
   "account.locked_info": "Essa conta está trancada. Se você a seguir sua solicitação será revisada manualmente.",
   "account.media": "Mídia",
@@ -25,7 +25,7 @@
   "account.mute": "Silenciar @{name}",
   "account.mute_notifications": "Silenciar notificações de @{name}",
   "account.muted": "Silenciado",
-  "account.never_active": "Never",
+  "account.never_active": "Nunca",
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots e respostas",
   "account.report": "Denunciar @{name}",
@@ -38,11 +38,11 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
+  "alert.rate_limited.message": "Por favor tente novamente após {retry_time, time, medium}.",
   "alert.rate_limited.title": "Rate limited",
   "alert.unexpected.message": "Um erro inesperado ocorreu.",
   "alert.unexpected.title": "Eita!",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "autosuggest_hashtag.per_week": "{count} por semana",
   "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
   "bundle_column_error.retry": "Tente novamente",
@@ -53,7 +53,7 @@
   "column.blocks": "Usuários bloqueados",
   "column.community": "Local",
   "column.direct": "Mensagens diretas",
-  "column.directory": "Browse profiles",
+  "column.directory": "Explorar perfis",
   "column.domain_blocks": "Domínios escondidos",
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
@@ -63,7 +63,7 @@
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
   "column.public": "Global",
-  "column.status": "Toot",
+  "column.status": "Publicar",
   "column_back_button.label": "Voltar",
   "column_header.hide_settings": "Esconder configurações",
   "column_header.moveLeft_settings": "Mover coluna para a esquerda",
@@ -101,8 +101,8 @@
   "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado. Você não vai ver conteúdo desse domínio em nenhuma das timelines públicas ou nas suas notificações. Seus seguidores desse domínio serão removidos.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Sair",
+  "confirmations.logout.message": "Tem certeza que deseja encerrar a sessão?",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
   "confirmations.redraft.confirm": "Apagar & usar como rascunho",
@@ -111,10 +111,14 @@
   "confirmations.reply.message": "Responder agora vai sobrescrever a mensagem que você está compondo. Você tem certeza que quer continuar?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
-  "directory.federated": "From known fediverse",
+  "conversation.delete": "Excluir conversa",
+  "conversation.mark_as_read": "Marcar como lida",
+  "conversation.open": "Ver conversa",
+  "conversation.with": "Com {names}",
+  "directory.federated": "De fediverso conhecido",
   "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "directory.new_arrivals": "Acabaram de chegar",
+  "directory.recently_active": "Reverta esta propriedade para seu valor padrão",
   "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo.",
   "embed.preview": "Aqui está uma previsão de como ficará:",
   "emoji_button.activity": "Atividades",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Atualizar em tempo real",
   "intervals.full.days": "{number, plural, one {# dia} other {# dias}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@@ -236,7 +240,7 @@
   "lists.new.title_placeholder": "Novo título da lista",
   "lists.search": "Procurar entre as pessoas que você segue",
   "lists.subheading": "Suas listas",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# novo item} other {# novos items}}",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Global",
   "navigation_bar.security": "Segurança",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "and {count, plural, one {# outro} other {# outros}}",
   "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
   "notification.follow": "{name} te seguiu",
   "notification.mention": "{name} te mencionou",
@@ -295,6 +299,7 @@
   "poll.refresh": "Atualizar",
   "poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
   "poll.vote": "Votar",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Adicionar uma enquete",
   "poll_button.remove_poll": "Remover enquete",
   "privacy.change": "Ajustar a privacidade da mensagem",
@@ -330,11 +335,11 @@
   "search_results.accounts": "Pessoas",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Pesquisar toots por seu conteúdo não está habilitado neste servidor Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "status.admin_account": "Abrir interface de moderação para @{name}",
   "status.admin_status": "Abrir esse status na interface de moderação",
-  "status.block": "Block @{name}",
+  "status.block": "Bloquear @{name}",
   "status.cancel_reblog_private": "Desfazer compartilhamento",
   "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
   "status.copy": "Copiar o link para o status",
@@ -385,19 +390,19 @@
   "time_remaining.moments": "Momentos restantes",
   "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {pessoa} other {pessoas}} falando sobre",
-  "trends.trending_now": "Trending now",
+  "trends.trending_now": "Em alta no momento",
   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Limite de envio de arquivos excedido.",
   "upload_error.poll": "Envio de arquivos não é permitido com enquetes.",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
-  "upload_form.edit": "Edit",
+  "upload_form.edit": "Editar",
   "upload_form.undo": "Remover",
-  "upload_modal.analyzing_picture": "Analyzing picture…",
-  "upload_modal.apply": "Apply",
-  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
-  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.analyzing_picture": "Analisando imagem…",
+  "upload_modal.apply": "Aplicar",
+  "upload_modal.description_placeholder": "Grave e cabisbaixo, o filho justo zelava pela querida mãe doente",
+  "upload_modal.detect_text": "Detectar texto da imagem",
   "upload_modal.edit_media": "Edit media",
   "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
   "upload_modal.preview_label": "Preview ({ratio})",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index feba8fd9a..b286dd622 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Responder agora irá reescrever a mensagem que estás a compor actualmente. Tens a certeza que queres continuar?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Recarregar",
   "poll.total_votes": "{contar, plural, um {# vote} outro {# votes}}",
   "poll.vote": "Votar",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Adicionar votação",
   "poll_button.remove_poll": "Remover votação",
   "privacy.change": "Ajustar a privacidade da mensagem",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index 038b8ddd4..8bda8c060 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Răspunzând la asta acum, mesajul pe care îl compui în prezent se va șterge. Ești sigur că vrei să continui?",
   "confirmations.unfollow.confirm": "Nu mai urmări",
   "confirmations.unfollow.message": "Ești sigur că nu mai vrei să îl urmărești pe {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Cine vede asta",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 69bd5a422..82194d2c6 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "При ответе текст набираемого сообщения будет перезаписан. Продолжить?",
   "confirmations.unfollow.confirm": "Отписаться",
   "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Обновить",
   "poll.total_votes": "{count, plural, one {# голос} few {# голоса} many {# голосов} other {# голосов}}",
   "poll.vote": "Голосовать",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Добавить опрос",
   "poll_button.remove_poll": "Удалить опрос",
   "privacy.change": "Изменить видимость статуса",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 89a472d89..07616931f 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -63,7 +63,7 @@
   "column.notifications": "Oboznámenia",
   "column.pins": "Pripnuté príspevky",
   "column.public": "Federovaná časová os",
-  "column.status": "Toot",
+  "column.status": "Príspevok",
   "column_back_button.label": "Späť",
   "column_header.hide_settings": "Skryť nastavenia",
   "column_header.moveLeft_settings": "Presuň stĺpec doľava",
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Odpovedaním akurát teraz prepíšeš správu, ktorú máš práve rozpísanú. Si si istý/á, že chceš pokračovať?",
   "confirmations.unfollow.confirm": "Nesleduj",
   "confirmations.unfollow.message": "Naozaj chceš prestať sledovať {name}?",
+  "conversation.delete": "Vymaž konverzáciu",
+  "conversation.mark_as_read": "Označ za prečítané",
+  "conversation.open": "Ukáž konverzáciu",
+  "conversation.with": "S {names}",
   "directory.federated": "Zo známého fedivesmíru",
   "directory.local": "Iba z {domain}",
   "directory.new_arrivals": "Nové príchody",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "Základné",
   "home.column_settings.show_reblogs": "Zobraziť povýšené",
   "home.column_settings.show_replies": "Ukázať odpovede",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Aktualizuj v reálnom čase",
   "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dní}}",
   "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodín}}",
   "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minút}}",
@@ -264,7 +268,7 @@
   "navigation_bar.preferences": "Voľby",
   "navigation_bar.public_timeline": "Federovaná časová os",
   "navigation_bar.security": "Zabezbečenie",
-  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.and_n_others": "a {count, plural,one {# ostatné} other {# ostatných}}",
   "notification.favourite": "{name} si obľúbil/a tvoj príspevok",
   "notification.follow": "{name} ťa začal/a následovať",
   "notification.mention": "{name} ťa spomenul/a",
@@ -295,6 +299,7 @@
   "poll.refresh": "Obnov",
   "poll.total_votes": "{count, plural, one {# hlas} few {# hlasov} many {# hlasov} other {# hlasov}}",
   "poll.vote": "Hlasuj",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Pridaj anketu",
   "poll_button.remove_poll": "Odstráň anketu",
   "privacy.change": "Uprav súkromie príspevku",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index d7d78c41c..86091e4a7 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
   "confirmations.unfollow.confirm": "Prenehaj slediti",
   "confirmations.unfollow.message": "Ali ste prepričani, da ne želite več slediti {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Osveži",
   "poll.total_votes": "{count, plural,one {# glas} other {# glasov}}",
   "poll.vote": "Glasuj",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Dodaj anketo",
   "poll_button.remove_poll": "Odstrani anketo",
   "privacy.change": "Prilagodi zasebnost statusa",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 0f851051c..66b939682 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Përgjigja tani do të shkaktojë mbishkrimin e mesazhit që po hartoni. Jeni i sigurt se doni të vazhdohet më tej?",
   "confirmations.unfollow.confirm": "Resht së ndjekuri",
   "confirmations.unfollow.message": "Jeni i sigurt se doni të mos ndiqet më {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Rregulloni privatësi gjendje",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index fb6a365ce..f9144348d 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Otprati",
   "confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Podesi status privatnosti",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 064934f54..c7adda11a 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "Отпрати",
   "confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Подеси статус приватности",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index f666a4b6e..449b226f0 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Om du svarar nu kommer det att ersätta meddelandet du håller på att skriva. Är du säker på att du vill fortsätta?",
   "confirmations.unfollow.confirm": "Sluta följa",
   "confirmations.unfollow.message": "Är du säker på att du vill sluta följa {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Ladda om",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Rösta",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Justera sekretess",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index 3caf301d0..c020bafb3 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "இப்போது பதில், தற்போது நீங்கள் உருவாக்கும் செய்தி மேலெழுதப்படும். நீங்கள் தொடர விரும்புகிறீர்களா?",
   "confirmations.unfollow.confirm": "பின்தொடராட்",
   "confirmations.unfollow.message": "நிச்சயமாக நீங்கள் பின்தொடர விரும்புகிறீர்களா {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "பத்துயிர்ப்ப?ட்டு",
   "poll.total_votes": "{count, plural, one {# vote} மற்ற {# votes}}",
   "poll.vote": "வாக்களி",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "வாக்கெடுப்பைச் சேர்க்கவும்",
   "poll_button.remove_poll": "வாக்கெடுப்பை அகற்று",
   "privacy.change": "நிலை தனியுரிமை",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 5827dbb3a..ad99afcf4 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "ఇప్పుడే ప్రత్యుత్తరం ఇస్తే మీరు ప్రస్తుతం వ్రాస్తున్న సందేశం తిరగరాయబడుతుంది. మీరు ఖచ్చితంగా కొనసాగించాలనుకుంటున్నారా?",
   "confirmations.unfollow.confirm": "అనుసరించవద్దు",
   "confirmations.unfollow.message": "{name}ను మీరు ఖచ్చితంగా అనుసరించవద్దనుకుంటున్నారా?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "నవీకరించు",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "ఎన్నుకోండి",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "ఒక ఎన్నికను చేర్చు",
   "poll_button.remove_poll": "ఎన్నికను తొలగించు",
   "privacy.change": "స్టేటస్ గోప్యతను సర్దుబాటు చేయండి",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 33eb315f1..da57280ce 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -63,7 +63,7 @@
   "column.notifications": "การแจ้งเตือน",
   "column.pins": "โพสต์ที่ปักหมุด",
   "column.public": "เส้นเวลาที่ติดต่อกับภายนอก",
-  "column.status": "Toot",
+  "column.status": "โพสต์",
   "column_back_button.label": "ย้อนกลับ",
   "column_header.hide_settings": "ซ่อนการตั้งค่า",
   "column_header.moveLeft_settings": "ย้ายคอลัมน์ไปทางซ้าย",
@@ -111,8 +111,12 @@
   "confirmations.reply.message": "การตอบกลับตอนนี้จะเขียนทับข้อความที่คุณกำลังเขียน คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?",
   "confirmations.unfollow.confirm": "เลิกติดตาม",
   "confirmations.unfollow.message": "คุณแน่ใจหรือไม่ว่าต้องการเลิกติดตาม {name}?",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
+  "conversation.delete": "ลบการสนทนา",
+  "conversation.mark_as_read": "ทำเครื่องหมายว่าอ่านแล้ว",
+  "conversation.open": "ดูการสนทนา",
+  "conversation.with": "กับ {names}",
+  "directory.federated": "จากเฟดิเวิร์สที่รู้จัก",
+  "directory.local": "จาก {domain} เท่านั้น",
   "directory.new_arrivals": "New arrivals",
   "directory.recently_active": "Recently active",
   "embed.instructions": "ฝังสถานะนี้ในเว็บไซต์ของคุณโดยคัดลอกโค้ดด้านล่าง",
@@ -170,7 +174,7 @@
   "home.column_settings.basic": "พื้นฐาน",
   "home.column_settings.show_reblogs": "แสดงการดัน",
   "home.column_settings.show_replies": "แสดงการตอบกลับ",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "อัปเดตตามเวลาจริง",
   "intervals.full.days": "{number, plural, other {# วัน}}",
   "intervals.full.hours": "{number, plural, other {# ชั่วโมง}}",
   "intervals.full.minutes": "{number, plural, other {# นาที}}",
@@ -295,6 +299,7 @@
   "poll.refresh": "รีเฟรช",
   "poll.total_votes": "{count, plural, other {# การลงคะแนน}}",
   "poll.vote": "ลงคะแนน",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "เพิ่มโพล",
   "poll_button.remove_poll": "เอาโพลออก",
   "privacy.change": "ปรับเปลี่ยนความเป็นส่วนตัวของสถานะ",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 2c4d820de..1b5ce07d5 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Şimdi yanıtlarken o an oluşturduğunuz mesajın üzerine yazılır. Devam etmek istediğinize emin misiniz?",
   "confirmations.unfollow.confirm": "Takibi kaldır",
   "confirmations.unfollow.message": "{name}'yi takipten çıkarmak istediğinizden emin misiniz?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "Yalnızca {domain} adresinden",
   "directory.new_arrivals": "Yeni gelenler",
@@ -295,6 +299,7 @@
   "poll.refresh": "Yenile",
   "poll.total_votes": "{count, plural, one {# oy} other {# oy}}",
   "poll.vote": "Oy ver",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Bir anket ekleyin",
   "poll_button.remove_poll": "Anket kaldır",
   "privacy.change": "Gönderi gizliliğini ayarla",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 6ccb20fc6..9a64b857f 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Поточна відповідь перезапише повідомлення, яке ви зараз пишете. Ви впевнені, що хочете продовжити?",
   "confirmations.unfollow.confirm": "Відписатися",
   "confirmations.unfollow.message": "Ви впевнені, що хочете відписатися від {name}?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "З відомого федесвіту",
   "directory.local": "Тільки з домену {domain}",
   "directory.new_arrivals": "Нові надходження",
@@ -295,6 +299,7 @@
   "poll.refresh": "Оновити",
   "poll.total_votes": "{count, plural, one {# голос} few {# голоси} many {# голосів} other {# голосів}}",
   "poll.vote": "Проголосувати",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Додати опитування",
   "poll_button.remove_poll": "Видалити опитування",
   "privacy.change": "Змінити видимість допису",
diff --git a/app/javascript/mastodon/locales/whitelist_es-AR.json b/app/javascript/mastodon/locales/whitelist_es-AR.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_es-AR.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_ga.json b/app/javascript/mastodon/locales/whitelist_ga.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_ga.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 2f0373d93..4057fefa9 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "回复此消息将会覆盖当前正在编辑的信息。确定继续吗?",
   "confirmations.unfollow.confirm": "取消关注",
   "confirmations.unfollow.message": "你确定要取消关注 {name} 吗?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "刷新",
   "poll.total_votes": "{count} 票",
   "poll.vote": "投票",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "发起投票",
   "poll_button.remove_poll": "移除投票",
   "privacy.change": "设置嘟文可见范围",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 0a42aa47f..93439a3a1 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
   "confirmations.unfollow.confirm": "取消關注",
   "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "調整私隱設定",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 82d7b6db5..b88d1b60d 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -111,6 +111,10 @@
   "confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?",
   "confirmations.unfollow.confirm": "取消關注",
   "confirmations.unfollow.message": "真的要取消關注 {name} 嗎?",
+  "conversation.delete": "Delete conversation",
+  "conversation.mark_as_read": "Mark as read",
+  "conversation.open": "View conversation",
+  "conversation.with": "With {names}",
   "directory.federated": "From known fediverse",
   "directory.local": "From {domain} only",
   "directory.new_arrivals": "New arrivals",
@@ -295,6 +299,7 @@
   "poll.refresh": "重新整理",
   "poll.total_votes": "{count, plural, one {# 個投票} other {# 個投票}}",
   "poll.vote": "投票",
+  "poll.voted": "You voted for this answer",
   "poll_button.add_poll": "建立投票",
   "poll_button.remove_poll": "移除投票",
   "privacy.change": "調整隱私狀態",
diff --git a/app/javascript/mastodon/reducers/blocks.js b/app/javascript/mastodon/reducers/blocks.js
new file mode 100644
index 000000000..1b6507163
--- /dev/null
+++ b/app/javascript/mastodon/reducers/blocks.js
@@ -0,0 +1,22 @@
+import Immutable from 'immutable';
+
+import {
+  BLOCKS_INIT_MODAL,
+} from '../actions/blocks';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    account_id: null,
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case BLOCKS_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'account_id'], action.account.get('id'));
+    });
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 268237846..b5dc81703 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -103,10 +103,13 @@ function clearAll(state) {
   });
 };
 
-function appendMedia(state, media) {
+function appendMedia(state, media, file) {
   const prevSize = state.get('media_attachments').size;
 
   return state.withMutations(map => {
+    if (media.get('type') === 'image') {
+      media = media.set('file', file);
+    }
     map.update('media_attachments', list => list.push(media));
     map.set('is_uploading', false);
     map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
@@ -321,7 +324,7 @@ export default function compose(state = initialState, action) {
   case COMPOSE_UPLOAD_REQUEST:
     return state.set('is_uploading', true);
   case COMPOSE_UPLOAD_SUCCESS:
-    return appendMedia(state, fromJS(action.media));
+    return appendMedia(state, fromJS(action.media), action.file);
   case COMPOSE_UPLOAD_FAIL:
     return state.set('is_uploading', false);
   case COMPOSE_UPLOAD_UNDO:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 0f4b209d4..b8d608888 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -15,6 +15,7 @@ import settings from './settings';
 import push_notifications from './push_notifications';
 import status_lists from './status_lists';
 import mutes from './mutes';
+import blocks from './blocks';
 import reports from './reports';
 import contexts from './contexts';
 import compose from './compose';
@@ -51,6 +52,7 @@ const reducers = {
   settings,
   push_notifications,
   mutes,
+  blocks,
   reports,
   contexts,
   compose,
diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js
index a96232dbd..4672e5097 100644
--- a/app/javascript/mastodon/reducers/mutes.js
+++ b/app/javascript/mastodon/reducers/mutes.js
@@ -7,7 +7,6 @@ import {
 
 const initialState = Immutable.Map({
   new: Immutable.Map({
-    isSubmitting: false,
     account: null,
     notifications: true,
   }),
@@ -17,7 +16,6 @@ export default function mutes(state = initialState, action) {
   switch (action.type) {
   case MUTES_INIT_MODAL:
     return state.withMutations((state) => {
-      state.setIn(['new', 'isSubmitting'], false);
       state.setIn(['new', 'account'], action.account);
       state.setIn(['new', 'notifications'], true);
     });
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index 45d3a5c51..6ba80bd6a 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -7,6 +7,8 @@ import {
   NOTIFICATIONS_CLEAR,
   NOTIFICATIONS_SCROLL_TOP,
   NOTIFICATIONS_LOAD_PENDING,
+  NOTIFICATIONS_MOUNT,
+  NOTIFICATIONS_UNMOUNT,
 } from '../actions/notifications';
 import {
   ACCOUNT_BLOCK_SUCCESS,
@@ -22,6 +24,7 @@ const initialState = ImmutableMap({
   items: ImmutableList(),
   hasMore: true,
   top: false,
+  mounted: false,
   unread: 0,
   isLoading: false,
 });
@@ -37,7 +40,7 @@ const notificationToMap = notification => ImmutableMap({
 const normalizeNotification = (state, notification, usePendingItems) => {
   const top = state.get('top');
 
-  if (usePendingItems || !top || !state.get('pendingItems').isEmpty()) {
+  if (usePendingItems || !state.get('pendingItems').isEmpty()) {
     return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1);
   }
 
@@ -63,7 +66,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
 
   return state.withMutations(mutable => {
     if (!items.isEmpty()) {
-      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('top') || !mutable.get('pendingItems').isEmpty());
+      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
 
       mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
         const lastIndex = 1 + list.findLastIndex(
@@ -134,6 +137,10 @@ export default function notifications(state = initialState, action) {
     return action.timeline === 'home' ?
       state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
       state;
+  case NOTIFICATIONS_MOUNT:
+    return state.set('mounted', true);
+  case NOTIFICATIONS_UNMOUNT:
+    return state.set('mounted', false);
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index f3ed2fc59..0d7222e10 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -40,7 +40,8 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
     if (timeline.endsWith(':pinned')) {
       mMap.set('items', statuses.map(status => status.get('id')));
     } else if (!statuses.isEmpty()) {
-      usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('top') || !mMap.get('pendingItems').isEmpty());
+      usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
+
       mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => {
         const newIds = statuses.map(status => status.get('id'));
 
@@ -63,7 +64,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 const updateTimeline = (state, timeline, status, usePendingItems) => {
   const top = state.getIn([timeline, 'top']);
 
-  if (usePendingItems || !top || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
+  if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) {
     if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) {
       return state;
     }
diff --git a/app/javascript/mastodon/utils/scrollbar.js b/app/javascript/mastodon/utils/scrollbar.js
new file mode 100644
index 000000000..6f0ee010b
--- /dev/null
+++ b/app/javascript/mastodon/utils/scrollbar.js
@@ -0,0 +1,36 @@
+import { isMobile } from '../is_mobile';
+
+/** @type {number | null} */
+let cachedScrollbarWidth = null;
+
+/**
+ * @return {number}
+ */
+const getActualScrollbarWidth = () => {
+  const outer = document.createElement('div');
+  outer.style.visibility = 'hidden';
+  outer.style.overflow = 'scroll';
+  document.body.appendChild(outer);
+
+  const inner = document.createElement('div');
+  outer.appendChild(inner);
+
+  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
+  outer.parentNode.removeChild(outer);
+
+  return scrollbarWidth;
+};
+
+/**
+ * @return {number}
+ */
+export const getScrollbarWidth = () => {
+  if (cachedScrollbarWidth !== null) {
+    return cachedScrollbarWidth;
+  }
+
+  const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth();
+  cachedScrollbarWidth = scrollbarWidth;
+
+  return scrollbarWidth;
+};
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 9418188a7..1229a7505 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -110,6 +110,16 @@ function main() {
       this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
     });
   });
+
+  delegate(document, '.sidebar__toggle__icon', 'click', () => {
+    const target = document.querySelector('.sidebar ul');
+
+    if (target.style.display === 'block') {
+      target.style.display = 'none';
+    } else {
+      target.style.display = 'block';
+    }
+  });
 }
 
 loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index e7114ed07..45305d696 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -310,6 +310,7 @@ html {
 .boost-modal,
 .confirmation-modal,
 .mute-modal,
+.block-modal,
 .report-modal,
 .embed-modal,
 .error-modal,
@@ -326,6 +327,7 @@ html {
 .boost-modal__action-bar,
 .confirmation-modal__action-bar,
 .mute-modal__action-bar,
+.block-modal__action-bar,
 .onboarding-modal__paginator,
 .error-modal__footer {
   background: darken($ui-base-color, 6%);
diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss
index faaffb30f..68cad0fde 100644
--- a/app/javascript/styles/mastodon/_mixins.scss
+++ b/app/javascript/styles/mastodon/_mixins.scss
@@ -22,24 +22,6 @@
   color: $darker-text-color;
   font-size: 14px;
   margin: 0;
-
-  &::-moz-focus-inner {
-    border: 0;
-  }
-
-  &::-moz-focus-inner,
-  &:focus,
-  &:active {
-    outline: 0 !important;
-  }
-
-  &:focus {
-    background: lighten($ui-base-color, 4%);
-  }
-
-  @media screen and (max-width: 600px) {
-    font-size: 16px;
-  }
 }
 
 @mixin search-popout {
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 61637ce96..1dd8b7954 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -17,109 +17,102 @@ $small-breakpoint: 960px;
 
 .rich-formatting {
   font-family: $font-sans-serif, sans-serif;
-  font-size: 16px;
+  font-size: 14px;
   font-weight: 400;
-  font-size: 16px;
-  line-height: 30px;
+  line-height: 1.7;
+  word-wrap: break-word;
   color: $darker-text-color;
-  padding-right: 10px;
 
   a {
     color: $highlight-text-color;
     text-decoration: underline;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
   }
 
   p,
   li {
-    font-family: $font-sans-serif, sans-serif;
-    font-size: 16px;
-    font-weight: 400;
-    font-size: 16px;
-    line-height: 30px;
-    margin-bottom: 12px;
     color: $darker-text-color;
+  }
 
-    a {
-      color: $highlight-text-color;
-      text-decoration: underline;
-    }
+  p {
+    margin-top: 0;
+    margin-bottom: .85em;
 
     &:last-child {
       margin-bottom: 0;
     }
   }
 
-  strong,
-  em {
+  strong {
     font-weight: 700;
-    color: lighten($darker-text-color, 10%);
+    color: $secondary-text-color;
   }
 
-  h1 {
-    font-family: $font-display, sans-serif;
-    font-size: 26px;
-    line-height: 30px;
-    font-weight: 500;
-    margin-bottom: 20px;
+  em {
+    font-style: italic;
     color: $secondary-text-color;
+  }
 
-    small {
-      font-family: $font-sans-serif, sans-serif;
-      display: block;
-      font-size: 18px;
-      font-weight: 400;
-      color: lighten($darker-text-color, 10%);
-    }
+  code {
+    font-size: 0.85em;
+    background: darken($ui-base-color, 8%);
+    border-radius: 4px;
+    padding: 0.2em 0.3em;
   }
 
-  h2 {
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
     font-family: $font-display, sans-serif;
-    font-size: 22px;
-    line-height: 26px;
+    margin-top: 1.275em;
+    margin-bottom: .85em;
     font-weight: 500;
-    margin-bottom: 20px;
     color: $secondary-text-color;
   }
 
+  h1 {
+    font-size: 2em;
+  }
+
+  h2 {
+    font-size: 1.75em;
+  }
+
   h3 {
-    font-family: $font-display, sans-serif;
-    font-size: 18px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+    font-size: 1.5em;
   }
 
   h4 {
-    font-family: $font-display, sans-serif;
-    font-size: 16px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+    font-size: 1.25em;
   }
 
-  h5 {
-    font-family: $font-display, sans-serif;
-    font-size: 14px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+  h5,
+  h6 {
+    font-size: 1em;
   }
 
-  h6 {
-    font-family: $font-display, sans-serif;
-    font-size: 12px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+  ul {
+    list-style: disc;
+  }
+
+  ol {
+    list-style: decimal;
   }
 
   ul,
   ol {
-    margin-left: 20px;
+    margin: 0;
+    padding: 0;
+    padding-left: 2em;
+    margin-bottom: 0.85em;
 
     &[type='a'] {
       list-style-type: lower-alpha;
@@ -130,31 +123,63 @@ $small-breakpoint: 960px;
     }
   }
 
-  ul {
-    list-style: disc;
-  }
-
-  ol {
-    list-style: decimal;
-  }
-
-  li > ol,
-  li > ul {
-    margin-top: 6px;
-  }
-
   hr {
     width: 100%;
     height: 0;
     border: 0;
-    border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
-    margin: 20px 0;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+    margin: 1.7em 0;
 
     &.spacer {
       height: 1px;
       border: 0;
     }
   }
+
+  table {
+    width: 100%;
+    border-collapse: collapse;
+    break-inside: auto;
+    margin-top: 24px;
+    margin-bottom: 32px;
+
+    thead tr,
+    tbody tr {
+      break-after: auto;
+      break-inside: avoid;
+      border-bottom: 1px solid lighten($ui-base-color, 4%);
+      font-size: 1em;
+      line-height: 1.625;
+      font-weight: 400;
+      text-align: left;
+      color: $darker-text-color;
+    }
+
+    thead tr {
+      border-bottom-width: 2px;
+      line-height: 1.5;
+      font-weight: 500;
+      color: $dark-text-color;
+    }
+
+    th,
+    td {
+      padding: 8px;
+      align-self: start;
+      align-items: start;
+
+      &.nowrap {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        width: 25%;
+      }
+    }
+  }
+
+  & > :first-child {
+    margin-top: 0;
+  }
 }
 
 .information-board {
@@ -416,7 +441,7 @@ $small-breakpoint: 960px;
   }
 
   &__call-to-action {
-    background: darken($ui-base-color, 4%);
+    background: $ui-base-color;
     border-radius: 4px;
     padding: 25px 40px;
     overflow: hidden;
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 529ed8666..de95d82bf 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -5,21 +5,66 @@ $content-width: 840px;
 .admin-wrapper {
   display: flex;
   justify-content: center;
-  height: 100%;
+  width: 100%;
+  min-height: 100vh;
 
   .sidebar-wrapper {
-    flex: 1 1 $sidebar-width;
-    height: 100%;
-    background: $ui-base-color;
-    display: flex;
-    justify-content: flex-end;
+    min-height: 100vh;
+    overflow: hidden;
+    pointer-events: none;
+    flex: 1 1 auto;
+
+    &__inner {
+      display: flex;
+      justify-content: flex-end;
+      background: $ui-base-color;
+      height: 100%;
+    }
   }
 
   .sidebar {
     width: $sidebar-width;
-    height: 100%;
     padding: 0;
-    overflow-y: auto;
+    pointer-events: auto;
+
+    &__toggle {
+      display: none;
+      background: lighten($ui-base-color, 8%);
+      height: 48px;
+
+      &__logo {
+        flex: 1 1 auto;
+
+        a {
+          display: inline-block;
+          padding: 15px;
+        }
+
+        svg {
+          fill: $primary-text-color;
+          height: 20px;
+          position: relative;
+          bottom: -2px;
+        }
+      }
+
+      &__icon {
+        display: block;
+        color: $darker-text-color;
+        text-decoration: none;
+        flex: 0 0 auto;
+        font-size: 20px;
+        padding: 15px;
+      }
+
+      a {
+        &:hover,
+        &:focus,
+        &:active {
+          background: lighten($ui-base-color, 12%);
+        }
+      }
+    }
 
     .logo {
       display: block;
@@ -52,6 +97,9 @@ $content-width: 840px;
         transition: all 200ms linear;
         transition-property: color, background-color;
         border-radius: 4px 0 0 4px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
 
         i.fa {
           margin-right: 5px;
@@ -99,12 +147,30 @@ $content-width: 840px;
   }
 
   .content-wrapper {
-    flex: 2 1 $content-width;
-    overflow: auto;
+    box-sizing: border-box;
+    width: 100%;
+    max-width: $content-width;
+    flex: 1 1 auto;
+  }
+
+  @media screen and (max-width: $content-width + $sidebar-width) {
+    .sidebar-wrapper--empty {
+      display: none;
+    }
+
+    .sidebar-wrapper {
+      width: $sidebar-width;
+      flex: 0 0 auto;
+    }
+  }
+
+  @media screen and (max-width: $no-columns-breakpoint) {
+    .sidebar-wrapper {
+      width: 100%;
+    }
   }
 
   .content {
-    max-width: $content-width;
     padding: 20px 15px;
     padding-top: 60px;
     padding-left: 25px;
@@ -123,6 +189,12 @@ $content-width: 840px;
       padding-bottom: 40px;
       border-bottom: 1px solid lighten($ui-base-color, 8%);
       margin-bottom: 40px;
+
+      @media screen and (max-width: $no-columns-breakpoint) {
+        border-bottom: 0;
+        padding-bottom: 0;
+        font-weight: 700;
+      }
     }
 
     h3 {
@@ -147,7 +219,7 @@ $content-width: 840px;
       font-size: 16px;
       color: $secondary-text-color;
       line-height: 28px;
-      font-weight: 400;
+      font-weight: 500;
     }
 
     .fields-group h6 {
@@ -176,7 +248,7 @@ $content-width: 840px;
 
     & > p {
       font-size: 14px;
-      line-height: 18px;
+      line-height: 21px;
       color: $secondary-text-color;
       margin-bottom: 20px;
 
@@ -208,20 +280,42 @@ $content-width: 840px;
 
   @media screen and (max-width: $no-columns-breakpoint) {
     display: block;
-    overflow-y: auto;
-    -webkit-overflow-scrolling: touch;
 
-    .sidebar-wrapper,
-    .content-wrapper {
-      flex: 0 0 auto;
-      height: auto;
-      overflow: initial;
+    .sidebar-wrapper {
+      min-height: 0;
     }
 
     .sidebar {
       width: 100%;
       padding: 0;
       height: auto;
+
+      &__toggle {
+        display: flex;
+      }
+
+      & > ul {
+        display: none;
+      }
+
+      ul a,
+      ul ul a {
+        border-radius: 0;
+        border-bottom: 1px solid lighten($ui-base-color, 4%);
+        transition: none;
+
+        &:hover {
+          transition: none;
+        }
+      }
+
+      ul ul {
+        border-radius: 0;
+      }
+
+      ul .simple-navigation-active-leaf a {
+        border-bottom-color: $ui-highlight-color;
+      }
     }
   }
 }
@@ -233,32 +327,35 @@ hr.spacer {
   height: 1px;
 }
 
-.muted-hint {
-  color: $darker-text-color;
+body,
+.admin-wrapper .content {
+  .muted-hint {
+    color: $darker-text-color;
 
-  a {
-    color: $highlight-text-color;
+    a {
+      color: $highlight-text-color;
+    }
   }
-}
 
-.positive-hint {
-  color: $valid-value-color;
-  font-weight: 500;
-}
+  .positive-hint {
+    color: $valid-value-color;
+    font-weight: 500;
+  }
 
-.negative-hint {
-  color: $error-value-color;
-  font-weight: 500;
-}
+  .negative-hint {
+    color: $error-value-color;
+    font-weight: 500;
+  }
 
-.neutral-hint {
-  color: $dark-text-color;
-  font-weight: 500;
-}
+  .neutral-hint {
+    color: $dark-text-color;
+    font-weight: 500;
+  }
 
-.warning-hint {
-  color: $gold-star;
-  font-weight: 500;
+  .warning-hint {
+    color: $gold-star;
+    font-weight: 500;
+  }
 }
 
 .filters {
@@ -267,10 +364,10 @@ hr.spacer {
 
   .filter-subset {
     flex: 0 0 auto;
-    margin: 0 40px 10px 0;
+    margin: 0 40px 20px 0;
 
     &:last-child {
-      margin-bottom: 20px;
+      margin-bottom: 30px;
     }
 
     ul {
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index f9332caa3..1f3ef7da2 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -86,9 +86,6 @@ body {
 
   &.admin {
     background: darken($ui-base-color, 4%);
-    position: fixed;
-    width: 100%;
-    height: 100%;
     padding: 0;
   }
 
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index dee3c3439..645a917ee 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -421,6 +421,10 @@
     border: 0;
     outline: 0;
 
+    &::placeholder {
+      color: $dark-text-color;
+    }
+
     &:focus {
       outline: 0;
     }
@@ -1272,14 +1276,28 @@
 
   &-composite {
     @include avatar-radius;
+    border-radius: 50%;
     overflow: hidden;
+    position: relative;
+    cursor: default;
 
     & > div {
-      @include avatar-radius;
       float: left;
       position: relative;
       box-sizing: border-box;
     }
+
+    &__label {
+      display: block;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      color: $primary-text-color;
+      text-shadow: 1px 1px 2px $base-shadow-color;
+      font-weight: 700;
+      font-size: 15px;
+    }
   }
 }
 
@@ -1511,6 +1529,7 @@ a.account__display-name {
 }
 
 .muted {
+  .status__content,
   .status__content p,
   .status__content a {
     color: $dark-text-color;
@@ -1889,6 +1908,7 @@ a.account__display-name {
       pointer-events: none;
       display: flex;
       justify-content: flex-end;
+      min-width: 285px;
 
       &--start {
         justify-content: flex-start;
@@ -1906,6 +1926,7 @@ a.account__display-name {
       box-sizing: border-box;
       width: 100%;
       max-width: 600px;
+      flex: 0 0 auto;
       display: flex;
       flex-direction: column;
 
@@ -3533,6 +3554,28 @@ a.status-card.compact:hover {
   .column-select {
     &__control {
       @include search-input;
+
+      &::placeholder {
+        color: lighten($darker-text-color, 4%);
+      }
+
+      &::-moz-focus-inner {
+        border: 0;
+      }
+
+      &::-moz-focus-inner,
+      &:focus,
+      &:active {
+        outline: 0 !important;
+      }
+
+      &:focus {
+        background: lighten($ui-base-color, 4%);
+      }
+
+      @media screen and (max-width: 600px) {
+        font-size: 16px;
+      }
     }
 
     &__placeholder {
@@ -4046,6 +4089,28 @@ a.status-card.compact:hover {
   padding-right: 30px;
   line-height: 18px;
   font-size: 16px;
+
+  &::placeholder {
+    color: lighten($darker-text-color, 4%);
+  }
+
+  &::-moz-focus-inner {
+    border: 0;
+  }
+
+  &::-moz-focus-inner,
+  &:focus,
+  &:active {
+    outline: 0 !important;
+  }
+
+  &:focus {
+    background: lighten($ui-base-color, 4%);
+  }
+
+  @media screen and (max-width: 600px) {
+    font-size: 16px;
+  }
 }
 
 .search__icon {
@@ -4468,7 +4533,8 @@ a.status-card.compact:hover {
 .confirmation-modal,
 .report-modal,
 .actions-modal,
-.mute-modal {
+.mute-modal,
+.block-modal {
   background: lighten($ui-secondary-color, 8%);
   color: $inverted-text-color;
   border-radius: 8px;
@@ -4522,7 +4588,8 @@ a.status-card.compact:hover {
 
 .boost-modal__action-bar,
 .confirmation-modal__action-bar,
-.mute-modal__action-bar {
+.mute-modal__action-bar,
+.block-modal__action-bar {
   display: flex;
   justify-content: space-between;
   background: $ui-secondary-color;
@@ -4550,11 +4617,13 @@ a.status-card.compact:hover {
   font-size: 14px;
 }
 
-.mute-modal {
+.mute-modal,
+.block-modal {
   line-height: 24px;
 }
 
-.mute-modal .react-toggle {
+.mute-modal .react-toggle,
+.block-modal .react-toggle {
   vertical-align: middle;
 }
 
@@ -4765,33 +4834,35 @@ a.status-card.compact:hover {
 }
 
 .confirmation-modal__action-bar,
-.mute-modal__action-bar {
-  .confirmation-modal__secondary-button,
-  .confirmation-modal__cancel-button,
-  .mute-modal__cancel-button {
-    background-color: transparent;
-    color: $lighter-text-color;
-    font-size: 14px;
-    font-weight: 500;
-
-    &:hover,
-    &:focus,
-    &:active {
-      color: darken($lighter-text-color, 4%);
-    }
-  }
-
+.mute-modal__action-bar,
+.block-modal__action-bar {
   .confirmation-modal__secondary-button {
     flex-shrink: 1;
   }
 }
 
+.confirmation-modal__secondary-button,
+.confirmation-modal__cancel-button,
+.mute-modal__cancel-button,
+.block-modal__cancel-button {
+  background-color: transparent;
+  color: $lighter-text-color;
+  font-size: 14px;
+  font-weight: 500;
+
+  &:hover,
+  &:focus,
+  &:active {
+    color: darken($lighter-text-color, 4%);
+  }
+}
+
 .confirmation-modal__container,
 .mute-modal__container,
+.block-modal__container,
 .report-modal__target {
   padding: 30px;
   font-size: 16px;
-  text-align: center;
 
   strong {
     font-weight: 500;
@@ -4804,6 +4875,31 @@ a.status-card.compact:hover {
   }
 }
 
+.confirmation-modal__container,
+.report-modal__target {
+  text-align: center;
+}
+
+.block-modal,
+.mute-modal {
+  &__explanation {
+    margin-top: 20px;
+  }
+
+  .setting-toggle {
+    margin-top: 20px;
+    margin-bottom: 24px;
+    display: flex;
+    align-items: center;
+
+    &__label {
+      color: $inverted-text-color;
+      margin: 0;
+      margin-left: 8px;
+    }
+  }
+}
+
 .report-modal__target {
   padding: 15px;
 
@@ -6333,48 +6429,63 @@ noscript {
   }
 }
 
-.layout-toggle {
+.conversation {
   display: flex;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
   padding: 5px;
+  padding-bottom: 0;
 
-  button {
-    box-sizing: border-box;
-    flex: 0 0 50%;
-    background: transparent;
-    padding: 5px;
-    border: 0;
-    position: relative;
+  &:focus {
+    background: lighten($ui-base-color, 2%);
+    outline: 0;
+  }
 
-    &:hover,
-    &:focus,
-    &:active {
-      svg path:first-child {
-        fill: lighten($ui-base-color, 16%);
-      }
-    }
+  &__avatar {
+    flex: 0 0 auto;
+    padding: 10px;
+    padding-top: 12px;
   }
 
-  svg {
-    width: 100%;
-    height: auto;
+  &__content {
+    flex: 1 1 auto;
+    padding: 10px 5px;
+    padding-right: 15px;
+    word-break: break-all;
+    overflow: hidden;
 
-    path:first-child {
-      fill: lighten($ui-base-color, 12%);
+    &__info {
+      overflow: hidden;
+      display: flex;
+      flex-direction: row-reverse;
+      justify-content: space-between;
     }
 
-    path:last-child {
-      fill: darken($ui-base-color, 14%);
+    &__relative-time {
+      font-size: 15px;
+      color: $darker-text-color;
+      padding-left: 15px;
     }
-  }
 
-  &__active {
-    color: $ui-highlight-color;
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    background: lighten($ui-base-color, 12%);
-    border-radius: 50%;
-    padding: 0.35rem;
+    &__names {
+      color: $darker-text-color;
+      font-size: 15px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      margin-bottom: 4px;
+      flex-basis: 170px;
+      flex-shrink: 1000;
+
+      a {
+        color: $primary-text-color;
+        text-decoration: none;
+
+        &:hover,
+        &:focus,
+        &:active {
+          text-decoration: underline;
+        }
+      }
+    }
   }
 }
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index aa45c0174..2d1bf1abd 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -141,6 +141,63 @@
     grid-row: 3;
   }
 
+  @media screen and (max-width: $no-gap-breakpoint) {
+    grid-gap: 0;
+    grid-template-columns: minmax(0, 100%);
+
+    .column-0 {
+      grid-column: 1;
+    }
+
+    .column-1 {
+      grid-column: 1;
+      grid-row: 3;
+    }
+
+    .column-2 {
+      grid-column: 1;
+      grid-row: 2;
+    }
+
+    .column-3 {
+      grid-column: 1;
+      grid-row: 4;
+    }
+  }
+}
+
+.grid-4 {
+  display: grid;
+  grid-gap: 10px;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  grid-auto-columns: 25%;
+  grid-auto-rows: max-content;
+
+  .column-0 {
+    grid-column: 1 / 5;
+    grid-row: 1;
+  }
+
+  .column-1 {
+    grid-column: 1 / 4;
+    grid-row: 2;
+  }
+
+  .column-2 {
+    grid-column: 4;
+    grid-row: 2;
+  }
+
+  .column-3 {
+    grid-column: 2 / 5;
+    grid-row: 3;
+  }
+
+  .column-4 {
+    grid-column: 1;
+    grid-row: 3;
+  }
+
   .landing-page__call-to-action {
     min-height: 100%;
   }
@@ -190,6 +247,11 @@
 
     .column-3 {
       grid-column: 1;
+      grid-row: 5;
+    }
+
+    .column-4 {
+      grid-column: 1;
       grid-row: 4;
     }
   }
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 16352340b..b729d912e 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -254,6 +254,10 @@ code {
       &-6 {
         max-width: 50%;
       }
+
+      .actions {
+        margin-top: 27px;
+      }
     }
 
     .fields-group:last-child,
@@ -334,6 +338,10 @@ code {
     border-radius: 4px;
     padding: 10px;
 
+    &::placeholder {
+      color: lighten($darker-text-color, 4%);
+    }
+
     &:invalid {
       box-shadow: none;
     }
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index e80220f27..f59a9d693 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -95,13 +95,19 @@
 
   &__number {
     display: inline-block;
-    width: 36px;
+    width: 52px;
     font-weight: 700;
     padding: 0 10px;
+    padding-left: 8px;
     text-align: right;
     margin-top: auto;
     margin-bottom: auto;
-    flex: 0 0 36px;
+    flex: 0 0 52px;
+  }
+
+  &__vote__mark {
+    float: left;
+    line-height: 18px;
   }
 
   &__footer {
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index d6403986f..5a6e10aa4 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -292,70 +292,3 @@ a.table-action-link {
     }
   }
 }
-
-.blocks-table {
-  width: 100%;
-  max-width: 100%;
-  border-spacing: 0;
-  border-collapse: collapse;
-  table-layout: fixed;
-  border: 1px solid darken($ui-base-color, 8%);
-
-  thead {
-    border: 1px solid darken($ui-base-color, 8%);
-    background: darken($ui-base-color, 4%);
-    font-weight: 500;
-
-    th.severity-column {
-      width: 120px;
-    }
-
-    th.button-column {
-      width: 23px;
-    }
-  }
-
-  tbody > tr {
-    border: 1px solid darken($ui-base-color, 8%);
-    border-bottom: 0;
-    background: darken($ui-base-color, 4%);
-
-    &:hover {
-      background: darken($ui-base-color, 2%);
-    }
-
-    &.even {
-      background: $ui-base-color;
-
-      &:hover {
-        background: lighten($ui-base-color, 2%);
-      }
-    }
-
-    &.rationale {
-      background: lighten($ui-base-color, 4%);
-      border-top: 0;
-
-      &:hover {
-        background: lighten($ui-base-color, 6%);
-      }
-
-      &.hidden {
-        display: none;
-      }
-    }
-
-    td:first-child {
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-  }
-
-  th,
-  td {
-    padding: 8px;
-    line-height: 18px;
-    vertical-align: top;
-    text-align: left;
-  }
-}
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index 04beb869c..ca050a8d9 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -128,41 +128,43 @@
   margin-bottom: 10px;
 }
 
-.contact-widget,
-.landing-page__information.contact-widget {
-  box-sizing: border-box;
-  padding: 20px;
-  min-height: 100%;
-  border-radius: 4px;
-  background: $ui-base-color;
-  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-}
-
 .contact-widget {
+  min-height: 100%;
   font-size: 15px;
   color: $darker-text-color;
   line-height: 20px;
   word-wrap: break-word;
   font-weight: 400;
+  padding: 0;
 
-  strong {
-    font-weight: 500;
+  h4 {
+    padding: 10px;
+    text-transform: uppercase;
+    font-weight: 700;
+    font-size: 13px;
+    color: $darker-text-color;
   }
 
-  p {
-    margin-bottom: 10px;
-
-    &:last-child {
-      margin-bottom: 0;
-    }
+  .account {
+    border-bottom: 0;
+    padding: 10px 0;
+    padding-top: 5px;
   }
 
-  &__mail {
-    margin-top: 10px;
+  & > a {
+    display: inline-block;
+    padding: 10px;
+    padding-top: 0;
+    color: $darker-text-color;
+    text-decoration: none;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
 
-    a {
-      color: $primary-text-color;
-      text-decoration: none;
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
     }
   }
 }
@@ -562,3 +564,38 @@ $fluid-breakpoint: $maximum-width + 20px;
     }
   }
 }
+
+.table-of-contents {
+  background: darken($ui-base-color, 4%);
+  min-height: 100%;
+  font-size: 14px;
+  border-radius: 4px;
+
+  li a {
+    display: block;
+    font-weight: 500;
+    padding: 15px;
+    overflow: hidden;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    text-decoration: none;
+    color: $primary-text-color;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+
+  li:last-child a {
+    border-bottom: 0;
+  }
+
+  li ul {
+    padding-left: 20px;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+  }
+}
diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb
index ae3c11b6a..81303b715 100644
--- a/app/lib/activity_tracker.rb
+++ b/app/lib/activity_tracker.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ActivityTracker
-  EXPIRE_AFTER = 90.days.seconds
+  EXPIRE_AFTER = 6.months.seconds
 
   class << self
     include Redisable
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index e69193b71..76bf9b2e5 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -232,25 +232,40 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       items    = @object['oneOf']
     end
 
+    voters_count = @object['votersCount']
+
     @account.polls.new(
       multiple: multiple,
       expires_at: expires_at,
       options: items.map { |item| item['name'].presence || item['content'] }.compact,
-      cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
+      cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 },
+      voters_count: voters_count
     )
   end
 
   def poll_vote?
     return false if replied_to_status.nil? || replied_to_status.preloadable_poll.nil? || !replied_to_status.local? || !replied_to_status.preloadable_poll.options.include?(@object['name'])
 
-    unless replied_to_status.preloadable_poll.expired?
-      replied_to_status.preloadable_poll.votes.create!(account: @account, choice: replied_to_status.preloadable_poll.options.index(@object['name']), uri: @object['id'])
-      ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals?
-    end
+    poll_vote! unless replied_to_status.preloadable_poll.expired?
 
     true
   end
 
+  def poll_vote!
+    poll = replied_to_status.preloadable_poll
+    already_voted = true
+    RedisLock.acquire(poll_lock_options) do |lock|
+      if lock.acquired?
+        already_voted = poll.votes.where(account: @account).exists?
+        poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: @object['id'])
+      else
+        raise Mastodon::RaceConditionError
+      end
+    end
+    increment_voters_count! unless already_voted
+    ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals?
+  end
+
   def resolve_thread(status)
     return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
     ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
@@ -416,7 +431,22 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
   end
 
+  def increment_voters_count!
+    poll = replied_to_status.preloadable_poll
+    unless poll.voters_count.nil?
+      poll.voters_count = poll.voters_count + 1
+      poll.save
+    end
+  rescue ActiveRecord::StaleObjectError
+    poll.reload
+    retry
+  end
+
   def lock_options
     { redis: Redis.current, key: "create:#{@object['id']}" }
   end
+
+  def poll_lock_options
+    { redis: Redis.current, key: "vote:#{replied_to_status.poll_id}:#{@account.id}" }
+  end
 end
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index 28f1da19f..ec92f4255 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -21,7 +21,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
 
     follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
 
-    if target_account.locked?
+    if target_account.locked? || @account.silenced?
       NotifyService.new.call(target_account, follow_request)
     else
       AuthorizeFollowService.new.call(@account, target_account)
diff --git a/app/lib/activitypub/activity/move.rb b/app/lib/activitypub/activity/move.rb
index 6c6a2b967..12bb82d25 100644
--- a/app/lib/activitypub/activity/move.rb
+++ b/app/lib/activitypub/activity/move.rb
@@ -19,11 +19,7 @@ class ActivityPub::Activity::Move < ActivityPub::Activity
     origin_account.update(moved_to_account: target_account)
 
     # Initiate a re-follow for each follower
-    origin_account.followers.local.select(:id).find_in_batches do |follower_accounts|
-      UnfollowFollowWorker.push_bulk(follower_accounts.map(&:id)) do |follower_account_id|
-        [follower_account_id, origin_account.id, target_account.id]
-      end
-    end
+    MoveWorker.perform_async(origin_account.id, target_account.id)
   end
 
   private
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index cb2ac72d4..2a8f72333 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -21,6 +21,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
     identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
     blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
     discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
+    voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
   }.freeze
 
   def self.default_key_transform
diff --git a/app/lib/nodeinfo/adapter.rb b/app/lib/nodeinfo/adapter.rb
new file mode 100644
index 000000000..1b48dcb98
--- /dev/null
+++ b/app/lib/nodeinfo/adapter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class NodeInfo::Adapter < ActiveModelSerializers::Adapter::Attributes
+  def self.default_key_transform
+    :camel_lower
+  end
+end
diff --git a/app/lib/toc_generator.rb b/app/lib/toc_generator.rb
new file mode 100644
index 000000000..0c8f766ca
--- /dev/null
+++ b/app/lib/toc_generator.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class TOCGenerator
+  TARGET_ELEMENTS = %w(h1 h2 h3 h4 h5 h6).freeze
+  LISTED_ELEMENTS = %w(h2 h3).freeze
+
+  class Section
+    attr_accessor :depth, :title, :children, :anchor
+
+    def initialize(depth, title, anchor)
+      @depth    = depth
+      @title    = title
+      @children = []
+      @anchor   = anchor
+    end
+
+    delegate :<<, to: :children
+  end
+
+  def initialize(source_html)
+    @source_html = source_html
+    @processed   = false
+    @target_html = ''
+    @headers     = []
+    @slugs       = Hash.new { |h, k| h[k] = 0 }
+  end
+
+  def html
+    parse_and_transform unless @processed
+    @target_html
+  end
+
+  def toc
+    parse_and_transform unless @processed
+    @headers
+  end
+
+  private
+
+  def parse_and_transform
+    return if @source_html.blank?
+
+    parsed_html = Nokogiri::HTML.fragment(@source_html)
+
+    parsed_html.traverse do |node|
+      next unless TARGET_ELEMENTS.include?(node.name)
+
+      anchor = node['id'] || node.text.parameterize.presence || 'sec'
+      @slugs[anchor] += 1
+      anchor = "#{anchor}-#{@slugs[anchor]}" if @slugs[anchor] > 1
+
+      node['id'] = anchor
+
+      next unless LISTED_ELEMENTS.include?(node.name)
+
+      depth          = node.name[1..-1]
+      latest_section = @headers.last
+
+      if latest_section.nil? || latest_section.depth >= depth
+        @headers << Section.new(depth, node.text, anchor)
+      else
+        latest_section << Section.new(depth, node.text, anchor)
+      end
+    end
+
+    @target_html = parsed_html.to_s
+    @processed   = true
+  end
+end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index b41004acc..6b81f6873 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -57,6 +57,39 @@ class UserMailer < Devise::Mailer
     end
   end
 
+  def two_factor_enabled(user, **)
+    @resource = user
+    @instance = Rails.configuration.x.local_domain
+
+    return if @resource.disabled?
+
+    I18n.with_locale(@resource.locale || I18n.default_locale) do
+      mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
+    end
+  end
+
+  def two_factor_disabled(user, **)
+    @resource = user
+    @instance = Rails.configuration.x.local_domain
+
+    return if @resource.disabled?
+
+    I18n.with_locale(@resource.locale || I18n.default_locale) do
+      mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
+    end
+  end
+
+  def two_factor_recovery_codes_changed(user, **)
+    @resource = user
+    @instance = Rails.configuration.x.local_domain
+
+    return if @resource.disabled?
+
+    I18n.with_locale(@resource.locale || I18n.default_locale) do
+      mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
+    end
+  end
+
   def welcome(user)
     @resource = user
     @instance = Rails.configuration.x.local_domain
diff --git a/app/models/account_alias.rb b/app/models/account_alias.rb
new file mode 100644
index 000000000..66f8ce409
--- /dev/null
+++ b/app/models/account_alias.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: account_aliases
+#
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)
+#  acct       :string           default(""), not null
+#  uri        :string           default(""), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class AccountAlias < ApplicationRecord
+  belongs_to :account
+
+  validates :acct, presence: true, domain: { acct: true }
+  validates :uri, presence: true
+  validates :uri, uniqueness: { scope: :account_id }
+
+  before_validation :set_uri
+  after_create :add_to_account
+  after_destroy :remove_from_account
+
+  def acct=(val)
+    val = val.to_s.strip
+    super(val.start_with?('@') ? val[1..-1] : val)
+  end
+
+  private
+
+  def set_uri
+    target_account = ResolveAccountService.new.call(acct)
+    self.uri       = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil?
+  rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+    # Validation will take care of it
+  end
+
+  def add_to_account
+    account.update(also_known_as: account.also_known_as + [uri])
+  end
+
+  def remove_from_account
+    account.update(also_known_as: account.also_known_as.reject { |x| x == uri })
+  end
+end
diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb
new file mode 100644
index 000000000..681b5b2cd
--- /dev/null
+++ b/app/models/account_migration.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: account_migrations
+#
+#  id                :bigint(8)        not null, primary key
+#  account_id        :bigint(8)
+#  acct              :string           default(""), not null
+#  followers_count   :bigint(8)        default(0), not null
+#  target_account_id :bigint(8)
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#
+
+class AccountMigration < ApplicationRecord
+  COOLDOWN_PERIOD = 30.days.freeze
+
+  belongs_to :account
+  belongs_to :target_account, class_name: 'Account'
+
+  before_validation :set_target_account
+  before_validation :set_followers_count
+
+  validates :acct, presence: true, domain: { acct: true }
+  validate :validate_migration_cooldown
+  validate :validate_target_account
+
+  scope :within_cooldown, ->(now = Time.now.utc) { where(arel_table[:created_at].gteq(now - COOLDOWN_PERIOD)) }
+
+  attr_accessor :current_password, :current_username
+
+  def save_with_challenge(current_user)
+    if current_user.encrypted_password.present?
+      errors.add(:current_password, :invalid) unless current_user.valid_password?(current_password)
+    else
+      errors.add(:current_username, :invalid) unless account.username == current_username
+    end
+
+    return false unless errors.empty?
+
+    save
+  end
+
+  def cooldown_at
+    created_at + COOLDOWN_PERIOD
+  end
+
+  def acct=(val)
+    super(val.to_s.strip.gsub(/\A@/, ''))
+  end
+
+  private
+
+  def set_target_account
+    self.target_account = ResolveAccountService.new.call(acct)
+  rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+    # Validation will take care of it
+  end
+
+  def set_followers_count
+    self.followers_count = account.followers_count
+  end
+
+  def validate_target_account
+    if target_account.nil?
+      errors.add(:acct, I18n.t('migrations.errors.not_found'))
+    else
+      errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account))
+      errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id
+      errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
+    end
+  end
+
+  def validate_migration_cooldown
+    errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
+  end
+end
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index f76cf305f..499edbf4e 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -53,6 +53,8 @@ module AccountAssociations
 
     # Account migrations
     belongs_to :moved_to_account, class_name: 'Account', optional: true
+    has_many :migrations, class_name: 'AccountMigration', dependent: :destroy, inverse_of: :account
+    has_many :aliases, class_name: 'AccountAlias', dependent: :destroy, inverse_of: :account
 
     # Hashtags
     has_and_belongs_to_many :tags
diff --git a/app/models/concerns/ldap_authenticable.rb b/app/models/concerns/ldap_authenticable.rb
index 84ff84c4b..117993947 100644
--- a/app/models/concerns/ldap_authenticable.rb
+++ b/app/models/concerns/ldap_authenticable.rb
@@ -3,24 +3,50 @@
 module LdapAuthenticable
   extend ActiveSupport::Concern
 
-  def ldap_setup(_attributes)
-    self.confirmed_at = Time.now.utc
-    self.admin        = false
-    self.external     = true
+  class_methods do
+    def authenticate_with_ldap(params = {})
+      ldap   = Net::LDAP.new(ldap_options)
+      filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: params[:email])
 
-    save!
-  end
+      if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
+        ldap_get_user(user_info.first)
+      end
+    end
 
-  class_methods do
     def ldap_get_user(attributes = {})
       resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
 
       if resource.blank?
-        resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first })
-        resource.ldap_setup(attributes)
+        resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }, admin: false, external: true, confirmed_at: Time.now.utc)
+        resource.save!
       end
 
       resource
     end
+
+    def ldap_options
+      opts = {
+        host: Devise.ldap_host,
+        port: Devise.ldap_port,
+        base: Devise.ldap_base,
+
+        auth: {
+          method: :simple,
+          username: Devise.ldap_bind_dn,
+          password: Devise.ldap_password,
+        },
+
+        connect_timeout: 10,
+      }
+
+      if [:simple_tls, :start_tls].include?(Devise.ldap_method)
+        opts[:encryption] = {
+          method: Devise.ldap_method,
+          tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.tap { |options| options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if Devise.ldap_tls_no_verify },
+        }
+      end
+
+      opts
+    end
   end
 end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 4383cbd05..4e865b850 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -26,6 +26,7 @@ class DomainBlock < ApplicationRecord
 
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
   scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
+  scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
 
   class << self
     def suspend?(domain)
diff --git a/app/models/form/challenge.rb b/app/models/form/challenge.rb
new file mode 100644
index 000000000..40c99649c
--- /dev/null
+++ b/app/models/form/challenge.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Form::Challenge
+  include ActiveModel::Model
+
+  attr_accessor :current_password, :current_username,
+                :return_to
+end
diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb
deleted file mode 100644
index c2a8655e1..000000000
--- a/app/models/form/migration.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-class Form::Migration
-  include ActiveModel::Validations
-
-  attr_accessor :acct, :account
-
-  def initialize(attrs = {})
-    @account = attrs[:account]
-    @acct    = attrs[:account].acct unless @account.nil?
-    @acct    = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
-  end
-
-  def valid?
-    return false unless super
-    set_account
-    errors.empty?
-  end
-
-  private
-
-  def set_account
-    self.account = (ResolveAccountService.new.call(acct) if account.nil? && acct.present?)
-  end
-end
diff --git a/app/models/form/redirect.rb b/app/models/form/redirect.rb
new file mode 100644
index 000000000..a7961f8e8
--- /dev/null
+++ b/app/models/form/redirect.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class Form::Redirect
+  include ActiveModel::Model
+
+  attr_accessor :account, :target_account, :current_password,
+                :current_username
+
+  attr_reader :acct
+
+  validates :acct, presence: true, domain: { acct: true }
+  validate :validate_target_account
+
+  def valid_with_challenge?(current_user)
+    if current_user.encrypted_password.present?
+      errors.add(:current_password, :invalid) unless current_user.valid_password?(current_password)
+    else
+      errors.add(:current_username, :invalid) unless account.username == current_username
+    end
+
+    return false unless errors.empty?
+
+    set_target_account
+    valid?
+  end
+
+  def acct=(val)
+    @acct = val.to_s.strip.gsub(/\A@/, '')
+  end
+
+  private
+
+  def set_target_account
+    @target_account = ResolveAccountService.new.call(acct)
+  rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
+    # Validation will take care of it
+  end
+
+  def validate_target_account
+    if target_account.nil?
+      errors.add(:acct, I18n.t('migrations.errors.not_found'))
+    else
+      errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id
+      errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id
+    end
+  end
+end
diff --git a/app/models/poll.rb b/app/models/poll.rb
index 8f72c7b11..5427368fd 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -16,6 +16,7 @@
 #  created_at      :datetime         not null
 #  updated_at      :datetime         not null
 #  lock_version    :integer          default(0), not null
+#  voters_count    :bigint(8)
 #
 
 class Poll < ApplicationRecord
@@ -54,6 +55,10 @@ class Poll < ApplicationRecord
     account.id == account_id || votes.where(account: account).exists?
   end
 
+  def own_votes(account)
+    votes.where(account: account).pluck(:choice)
+  end
+
   delegate :local?, to: :account
 
   def remote?
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 9d6c1938a..4e89fbf85 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -47,6 +47,10 @@ class PreviewCard < ApplicationRecord
 
   before_save :extract_dimensions, if: :link?
 
+  def missing_image?
+    width.present? && height.present? && image_file_name.blank?
+  end
+
   def save_with_optional_image!
     save!
   rescue ActiveRecord::RecordInvalid
diff --git a/app/models/relay.rb b/app/models/relay.rb
index 6934a5c62..8c8a97db3 100644
--- a/app/models/relay.rb
+++ b/app/models/relay.rb
@@ -12,8 +12,6 @@
 #
 
 class Relay < ApplicationRecord
-  PRESET_RELAY = 'https://relay.joinmastodon.org/inbox'
-
   validates :inbox_url, presence: true, uniqueness: true, url: true, if: :will_save_change_to_inbox_url?
 
   enum state: [:idle, :pending, :accepted, :rejected]
@@ -74,7 +72,6 @@ class Relay < ApplicationRecord
   end
 
   def ensure_disabled
-    return unless enabled?
-    disable!
+    disable! if enabled?
   end
 end
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 52dd3f67b..5ea535287 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -49,7 +49,7 @@ class RemoteFollow
   end
 
   def fetch_template!
-    return missing_resource if acct.blank?
+    return missing_resource_error if acct.blank?
 
     _, domain = acct.split('@')
 
diff --git a/app/models/status.rb b/app/models/status.rb
index b0f610aa2..202434db3 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -360,7 +360,7 @@ class Status < ApplicationRecord
     end
 
     def reblogs_map(status_ids, account_id)
-      select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
+      unscoped.select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
     end
 
     def mutes_map(conversation_ids, account_id)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index b52b9bc9f..9aca3983f 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -124,16 +124,15 @@ class Tag < ApplicationRecord
       end
     end
 
-    def search_for(term, limit = 5, offset = 0)
+    def search_for(term, limit = 5, offset = 0, options = {})
       normalized_term = normalize(term.strip).mb_chars.downcase.to_s
       pattern         = sanitize_sql_like(normalized_term) + '%'
+      query           = Tag.listable.where(arel_table[:name].lower.matches(pattern))
+      query           = query.where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil))) if options[:exclude_unreviewed]
 
-      Tag.listable
-         .where(arel_table[:name].lower.matches(pattern))
-         .where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil)))
-         .order(Arel.sql('length(name) ASC, name ASC'))
-         .limit(limit)
-         .offset(offset)
+      query.order(Arel.sql('length(name) ASC, name ASC'))
+           .limit(limit)
+           .offset(offset)
     end
 
     def find_normalized(name)
diff --git a/app/models/user.rb b/app/models/user.rb
index 792246b1d..1556f677d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -169,7 +169,7 @@ class User < ApplicationRecord
   end
 
   def functional?
-    confirmed? && approved? && !disabled? && !account.suspended?
+    confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
   end
 
   def unconfirmed_or_pending?
@@ -265,17 +265,20 @@ class User < ApplicationRecord
   end
 
   def password_required?
-    return false if Devise.pam_authentication || Devise.ldap_authentication
+    return false if external?
+
     super
   end
 
   def send_reset_password_instructions
-    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
+    return false if encrypted_password.blank?
+
     super
   end
 
   def reset_password!(new_password, new_password_confirmation)
-    return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
+    return false if encrypted_password.blank?
+
     super
   end
 
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index b57807d1c..c5dbb58ba 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -20,6 +20,10 @@ class Web::PushSubscription < ApplicationRecord
 
   has_one :session_activation, foreign_key: 'web_push_subscription_id', inverse_of: :web_push_subscription
 
+  validates :endpoint, presence: true
+  validates :key_p256dh, presence: true
+  validates :key_auth, presence: true
+
   def push(notification)
     I18n.with_locale(associated_user&.locale || I18n.default_locale) do
       push_payload(payload_for_notification(notification), 48.hours.seconds)
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index d8a21fa7c..2b3849bd9 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -20,8 +20,8 @@ class InstancePresenter
     Rails.cache.fetch('user_count') { User.confirmed.joins(:account).merge(Account.without_suspended).count }
   end
 
-  def active_user_count
-    Rails.cache.fetch('active_user_count') { Redis.current.pfcount(*(0..3).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
+  def active_user_count(weeks = 4)
+    Rails.cache.fetch('active_user_count') { Redis.current.pfcount(*(0...weeks).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
   end
 
   def status_count
diff --git a/app/serializers/activitypub/move_serializer.rb b/app/serializers/activitypub/move_serializer.rb
new file mode 100644
index 000000000..5675875fa
--- /dev/null
+++ b/app/serializers/activitypub/move_serializer.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ActivityPub::MoveSerializer < ActivityPub::Serializer
+  attributes :id, :type, :target, :actor
+  attribute :virtual_object, key: :object
+
+  def id
+    [ActivityPub::TagManager.instance.uri_for(object.account), '#moves/', object.id].join
+  end
+
+  def type
+    'Move'
+  end
+
+  def target
+    ActivityPub::TagManager.instance.uri_for(object.target_account)
+  end
+
+  def virtual_object
+    ActivityPub::TagManager.instance.uri_for(object.account)
+  end
+
+  def actor
+    ActivityPub::TagManager.instance.uri_for(object.account)
+  end
+end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index f1cebbcd4..dce506fa2 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ActivityPub::NoteSerializer < ActivityPub::Serializer
-  context_extensions :atom_uri, :conversation, :sensitive
+  context_extensions :atom_uri, :conversation, :sensitive, :voters_count
 
   attributes :id, :type, :summary,
              :in_reply_to, :published, :url,
@@ -23,6 +23,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
   attribute :end_time, if: :poll_and_expires?
   attribute :closed, if: :poll_and_expired?
 
+  attribute :voters_count, if: :poll_and_voters_count?
+
   def id
     raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only]
     ActivityPub::TagManager.instance.uri_for(object)
@@ -142,6 +144,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
 
   alias end_time closed
 
+  def voters_count
+    object.preloadable_poll.voters_count
+  end
+
   def poll_and_expires?
     object.preloadable_poll&.expires_at&.present?
   end
@@ -150,6 +156,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
     object.preloadable_poll&.expired?
   end
 
+  def poll_and_voters_count?
+    object.preloadable_poll&.voters_count
+  end
+
   class MediaAttachmentSerializer < ActivityPub::Serializer
     context_extensions :blurhash, :focal_point
 
diff --git a/app/serializers/nodeinfo/discovery_serializer.rb b/app/serializers/nodeinfo/discovery_serializer.rb
new file mode 100644
index 000000000..07ab2a6ee
--- /dev/null
+++ b/app/serializers/nodeinfo/discovery_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class NodeInfo::DiscoverySerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attribute :links
+
+  def links
+    [{ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', href: nodeinfo_schema_url }]
+  end
+end
diff --git a/app/serializers/nodeinfo/serializer.rb b/app/serializers/nodeinfo/serializer.rb
new file mode 100644
index 000000000..1a7d7a911
--- /dev/null
+++ b/app/serializers/nodeinfo/serializer.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class NodeInfo::Serializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :version, :software, :protocols, :usage
+
+  def version
+    '2.0'
+  end
+
+  def software
+    { name: 'mastodon', version: Mastodon::Version.to_s }
+  end
+
+  def services
+    { outbound: [], inbound: [] }
+  end
+
+  def protocols
+    %w(activitypub)
+  end
+
+  def usage
+    {
+      users: {
+        total: instance_presenter.user_count,
+        active_month: instance_presenter.active_user_count(4),
+        active_halfyear: instance_presenter.active_user_count(24),
+      },
+
+      local_posts: instance_presenter.status_count,
+    }
+  end
+
+  private
+
+  def instance_presenter
+    @instance_presenter ||= InstancePresenter.new
+  end
+end
diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb
index fb195eb07..be0d763dc 100644
--- a/app/serializers/rest/credential_account_serializer.rb
+++ b/app/serializers/rest/credential_account_serializer.rb
@@ -12,6 +12,7 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer
       language: user.setting_default_language,
       note: object.note,
       fields: object.fields.map(&:to_h),
+      follow_requests_count: FollowRequest.where(target_account: object).limit(40).count,
     }
   end
 end
diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb
index 356c45b83..df6ebd0d4 100644
--- a/app/serializers/rest/poll_serializer.rb
+++ b/app/serializers/rest/poll_serializer.rb
@@ -2,12 +2,13 @@
 
 class REST::PollSerializer < ActiveModel::Serializer
   attributes :id, :expires_at, :expired,
-             :multiple, :votes_count
+             :multiple, :votes_count, :voters_count
 
   has_many :loaded_options, key: :options
   has_many :emojis, serializer: REST::CustomEmojiSerializer
 
   attribute :voted, if: :current_user?
+  attribute :own_votes, if: :current_user?
 
   def id
     object.id.to_s
@@ -21,6 +22,10 @@ class REST::PollSerializer < ActiveModel::Serializer
     object.voted?(current_user.account)
   end
 
+  def own_votes
+    object.own_votes(current_user.account)
+  end
+
   def current_user?
     !current_user.nil?
   end
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
index 680d9de6f..e39b2b372 100644
--- a/app/serializers/rss/account_serializer.rb
+++ b/app/serializers/rss/account_serializer.rb
@@ -25,7 +25,7 @@ class RSS::AccountSerializer
             .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str)
 
         status.media_attachments.each do |media|
-          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size)
+          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
         end
       end
     end
diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb
index e8562ee87..6737fb2c9 100644
--- a/app/serializers/rss/tag_serializer.rb
+++ b/app/serializers/rss/tag_serializer.rb
@@ -23,7 +23,7 @@ class RSS::TagSerializer
             .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str)
 
         status.media_attachments.each do |media|
-          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, length: media.file.size)
+          item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
         end
       end
     end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index 01caaefa9..40c5f8590 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -42,11 +42,9 @@ class AccountSearchService < BaseService
     return [] if limit_for_non_exact_results.zero?
 
     @search_results ||= begin
-      if Chewy.enabled?
-        from_elasticsearch
-      else
-        from_database
-      end
+      results = from_elasticsearch if Chewy.enabled?
+      results ||= from_database
+      results
     end
   end
 
@@ -92,6 +90,8 @@ class AccountSearchService < BaseService
     ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
 
     records
+  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
+    nil
   end
 
   def reputation_score_function
diff --git a/app/services/activitypub/process_poll_service.rb b/app/services/activitypub/process_poll_service.rb
index 2fbce65b9..cb4a0d460 100644
--- a/app/services/activitypub/process_poll_service.rb
+++ b/app/services/activitypub/process_poll_service.rb
@@ -28,6 +28,8 @@ class ActivityPub::ProcessPollService < BaseService
       end
     end
 
+    voters_count = @json['votersCount']
+
     latest_options = items.map { |item| item['name'].presence || item['content'] }
 
     # If for some reasons the options were changed, it invalidates all previous
@@ -39,7 +41,8 @@ class ActivityPub::ProcessPollService < BaseService
         last_fetched_at: Time.now.utc,
         expires_at: expires_at,
         options: latest_options,
-        cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
+        cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 },
+        voters_count: voters_count
       )
     rescue ActiveRecord::StaleObjectError
       poll.reload
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 4e75c370f..ac5503d46 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -22,7 +22,7 @@ class FetchLinkCardService < BaseService
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
         @card = PreviewCard.find_by(url: @url)
-        process_url if @card.nil? || @card.updated_at <= 2.weeks.ago
+        process_url if @card.nil? || @card.updated_at <= 2.weeks.ago || @card.missing_image?
       else
         raise Mastodon::RaceConditionError
       end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 101acdaf9..1941c2e2d 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -30,7 +30,7 @@ class FollowService < BaseService
 
     ActivityTracker.increment('activity:interactions')
 
-    if target_account.locked? || target_account.activitypub?
+    if target_account.locked? || source_account.silenced? || target_account.activitypub?
       request_follow(source_account, target_account, reblogs: reblogs)
     elsif target_account.local?
       direct_follow(source_account, target_account, reblogs: reblogs)
diff --git a/app/services/move_service.rb b/app/services/move_service.rb
new file mode 100644
index 000000000..da0c62c4e
--- /dev/null
+++ b/app/services/move_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class MoveService < BaseService
+  def call(migration)
+    @migration      = migration
+    @source_account = migration.account
+    @target_account = migration.target_account
+
+    update_redirect!
+    process_local_relationships!
+    distribute_update!
+    distribute_move!
+  end
+
+  private
+
+  def update_redirect!
+    @source_account.update!(moved_to_account: @target_account)
+  end
+
+  def process_local_relationships!
+    MoveWorker.perform_async(@source_account.id, @target_account.id)
+  end
+
+  def distribute_update!
+    ActivityPub::UpdateDistributionWorker.perform_async(@source_account.id)
+  end
+
+  def distribute_move!
+    ActivityPub::MoveDistributionWorker.perform_async(@migration.id)
+  end
+end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 5d17f111b..936e6ac55 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -184,7 +184,7 @@ class PostStatusService < BaseService
   def poll_attributes
     return if @options[:poll].blank?
 
-    @options[:poll].merge(account: @account)
+    @options[:poll].merge(account: @account, voters_count: 0)
   end
 
   def scheduled_options
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index a5ba5dd11..3a498dcf4 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -60,7 +60,8 @@ class SearchService < BaseService
     TagSearchService.new.call(
       @query,
       limit: @limit,
-      offset: @offset
+      offset: @offset,
+      exclude_unreviewed: @options[:exclude_unreviewed]
     )
   end
 
diff --git a/app/services/tag_search_service.rb b/app/services/tag_search_service.rb
index 64dd76bb7..b78d65625 100644
--- a/app/services/tag_search_service.rb
+++ b/app/services/tag_search_service.rb
@@ -2,15 +2,15 @@
 
 class TagSearchService < BaseService
   def call(query, options = {})
-    @query  = query.strip.gsub(/\A#/, '')
-    @offset = options[:offset].to_i
-    @limit  = options[:limit].to_i
+    @query   = query.strip.gsub(/\A#/, '')
+    @offset  = options.delete(:offset).to_i
+    @limit   = options.delete(:limit).to_i
+    @options = options
 
-    if Chewy.enabled?
-      from_elasticsearch
-    else
-      from_database
-    end
+    results   = from_elasticsearch if Chewy.enabled?
+    results ||= from_database
+
+    results
   end
 
   private
@@ -63,9 +63,9 @@ class TagSearchService < BaseService
           },
 
           {
-            term: {
+            match: {
               name: {
-                value: @query,
+                query: @query,
               },
             },
           },
@@ -73,10 +73,15 @@ class TagSearchService < BaseService
       },
     }
 
-    TagsIndex.query(query).filter(filter).limit(@limit).offset(@offset).objects.compact
+    definition = TagsIndex.query(query)
+    definition = definition.filter(filter) if @options[:exclude_unreviewed]
+
+    definition.limit(@limit).offset(@offset).objects.compact
+  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
+    nil
   end
 
   def from_database
-    Tag.search_for(@query, @limit, @offset)
+    Tag.search_for(@query, @limit, @offset, @options)
   end
 end
diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb
index 01756a73d..ebf24be37 100644
--- a/app/services/update_account_service.rb
+++ b/app/services/update_account_service.rb
@@ -20,7 +20,9 @@ class UpdateAccountService < BaseService
   private
 
   def authorize_all_follow_requests(account)
-    AuthorizeFollowWorker.push_bulk(FollowRequest.where(target_account: account).select(:account_id, :target_account_id)) do |req|
+    follow_requests = FollowRequest.where(target_account: account)
+    follow_requests = follow_requests.select { |req| !req.account.silenced? }
+    AuthorizeFollowWorker.push_bulk(follow_requests) do |req|
       [req.account_id, req.target_account_id]
     end
   end
diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb
index 0eeb8fd56..cb7dce6e8 100644
--- a/app/services/vote_service.rb
+++ b/app/services/vote_service.rb
@@ -12,12 +12,24 @@ class VoteService < BaseService
     @choices = choices
     @votes   = []
 
-    ApplicationRecord.transaction do
-      @choices.each do |choice|
-        @votes << @poll.votes.create!(account: @account, choice: choice)
+    already_voted = true
+
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        already_voted = @poll.votes.where(account: @account).exists?
+
+        ApplicationRecord.transaction do
+          @choices.each do |choice|
+            @votes << @poll.votes.create!(account: @account, choice: choice)
+          end
+        end
+      else
+        raise Mastodon::RaceConditionError
       end
     end
 
+    increment_voters_count! unless already_voted
+
     ActivityTracker.increment('activity:interactions')
 
     if @poll.account.local?
@@ -53,4 +65,18 @@ class VoteService < BaseService
   def build_json(vote)
     Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer))
   end
+
+  def increment_voters_count!
+    unless @poll.voters_count.nil?
+      @poll.voters_count = @poll.voters_count + 1
+      @poll.save
+    end
+  rescue ActiveRecord::StaleObjectError
+    @poll.reload
+    retry
+  end
+
+  def lock_options
+    { redis: Redis.current, key: "vote:#{@poll.id}:#{@account.id}" }
+  end
 end
diff --git a/app/views/about/_domain_blocks.html.haml b/app/views/about/_domain_blocks.html.haml
new file mode 100644
index 000000000..940bcb934
--- /dev/null
+++ b/app/views/about/_domain_blocks.html.haml
@@ -0,0 +1,10 @@
+%table
+  %thead
+    %tr
+      %th= t('about.unavailable_content_description.domain')
+      %th= t('about.unavailable_content_description.reason')
+  %tbody
+    - domain_blocks.each do |domain_block|
+      %tr
+        %td.nowrap= domain_block.domain
+        %td= domain_block.public_comment if display_blocks_rationale?
diff --git a/app/views/about/blocks.html.haml b/app/views/about/blocks.html.haml
deleted file mode 100644
index a81a4d1eb..000000000
--- a/app/views/about/blocks.html.haml
+++ /dev/null
@@ -1,48 +0,0 @@
-- content_for :page_title do
-  = t('domain_blocks.title', instance: site_hostname)
-
-.grid
-  .column-0
-    .box-widget.rich-formatting
-      %h2= t('domain_blocks.blocked_domains')
-      %p= t('domain_blocks.description', instance: site_hostname)
-      .table-wrapper
-        %table.blocks-table
-          %thead
-            %tr
-              %th= t('domain_blocks.domain')
-              %th.severity-column= t('domain_blocks.severity')
-              - if @show_rationale
-                %th.button-column
-          %tbody
-            - if @blocks.empty?
-              %tr
-                %td{ colspan: @show_rationale ? 3 : 2 }= t('domain_blocks.no_domain_blocks')
-            - else
-              - @blocks.each_with_index do |block, i|
-                %tr{ class: i % 2 == 0 ? 'even': nil }
-                  %td{ title: block.domain }= block.domain
-                  %td= block_severity_text(block)
-                  - if @show_rationale
-                    %td
-                      - if block.public_comment.present?
-                        %button.icon-button{ title: t('domain_blocks.show_rationale'), 'aria-label' => t('domain_blocks.show_rationale') }
-                          = fa_icon 'chevron-down fw', 'aria-hidden' => true
-                - if @show_rationale
-                  - if block.public_comment.present?
-                    %tr.rationale.hidden
-                      %td{ colspan: 3 }= block.public_comment.presence
-      %h2= t('domain_blocks.severity_legend.title')
-      - if @blocks.any? { |block| block.reject_media? }
-        %h3= t('domain_blocks.media_block')
-        %p= t('domain_blocks.severity_legend.media_block')
-      - if @blocks.any? { |block| block.severity == 'silence' }
-        %h3= t('domain_blocks.silence')
-        %p= t('domain_blocks.severity_legend.silence')
-      - if @blocks.any? { |block| block.severity == 'suspend' }
-        %h3= t('domain_blocks.suspension')
-        %p= t('domain_blocks.severity_legend.suspension')
-        - if public_fetch_mode?
-          %p= t('domain_blocks.severity_legend.suspension_disclaimer')
-  .column-1
-    = render 'application/sidebar'
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 4922f0f54..03be3f423 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -4,7 +4,7 @@
 - content_for :header_tags do
   = render partial: 'shared/og'
 
-.grid-3
+.grid-4
   .column-0
     .public-account-header.public-account-header--no-bar
       .public-account-header__image
@@ -27,22 +27,53 @@
             = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'), alt: ''
 
   .column-2
-    .landing-page__information.contact-widget
-      %p
-        %strong= t 'about.administered_by'
+    .contact-widget
+      %h4= t 'about.administered_by'
 
       = account_link_to(@instance_presenter.contact_account)
 
       - if @instance_presenter.site_contact_email.present?
-        %p.contact-widget__mail
-          %strong
-            = succeed ':' do
-              = t 'about.contact'
-          %br/
-          = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
+        %h4
+          = succeed ':' do
+            = t 'about.contact'
+
+        = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
 
   .column-3
     = render 'application/flashes'
 
-    .box-widget
-      .rich-formatting= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html')
+    - if @contents.blank? && (!display_blocks? || @blocks&.empty?)
+      = nothing_here
+    - else
+      .box-widget
+        .rich-formatting
+          = @contents.html_safe
+
+          - if display_blocks? && !@blocks.empty?
+            %h2#unavailable-content= t('about.unavailable_content')
+
+            %p= t('about.unavailable_content_html')
+
+            - if (blocks = @blocks.select(&:reject_media?)) && !blocks.empty?
+              %p= t('about.unavailable_content_description.rejecting_media')
+              = render partial: 'domain_blocks', locals: { domain_blocks: blocks }
+            - if (blocks = @blocks.select(&:silence?)) && !blocks.empty?
+              %p= t('about.unavailable_content_description.silenced')
+              = render partial: 'domain_blocks', locals: { domain_blocks: blocks }
+            - if (blocks = @blocks.select(&:suspend?)) && !blocks.empty?
+              %p= t('about.unavailable_content_description.suspended')
+              = render partial: 'domain_blocks', locals: { domain_blocks: blocks }
+
+  .column-4
+    %ul.table-of-contents
+      - @table_of_contents.each do |item|
+        %li
+          = link_to item.title, "##{item.anchor}"
+
+          - unless item.children.empty?
+            %ul
+              - item.children.each do |sub_item|
+                %li= link_to sub_item.title, "##{sub_item.anchor}"
+
+      - if display_blocks? && !@blocks.empty?
+        %li= link_to t('about.unavailable_content'), '#unavailable-content'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 06f29b79b..249e12563 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -4,35 +4,43 @@
 .dashboard__counters
   %div
     = link_to admin_accounts_url(local: 1, recent: 1) do
-      .dashboard__counters__num= number_to_human @users_count, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) }
+        = number_to_human @users_count, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.total_users'
   %div
     %div
-      .dashboard__counters__num= number_to_human @registrations_week, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) }
+        = number_to_human @registrations_week, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.week_users_new'
   %div
     %div
-      .dashboard__counters__num= number_to_human @logins_week, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) }
+        = number_to_human @logins_week, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.week_users_active'
   %div
     = link_to admin_pending_accounts_path do
-      .dashboard__counters__num= number_to_human @pending_users_count, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) }
+        = number_to_human @pending_users_count, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.pending_users'
   %div
     = link_to admin_reports_url do
-      .dashboard__counters__num= number_to_human @reports_count, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) }
+        = number_to_human @reports_count, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.open_reports'
   %div
     = link_to admin_tags_path(pending_review: '1') do
-      .dashboard__counters__num= number_to_human @pending_tags_count, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) }
+        = number_to_human @pending_tags_count, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.pending_tags'
   %div
     %div
-      .dashboard__counters__num= number_to_human @interactions_week, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) }
+        = number_to_human @interactions_week, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.week_interactions'
   %div
     = link_to sidekiq_url do
-      .dashboard__counters__num= number_to_human @queue_backlog, strip_insignificant_zeros: true
+      .dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) }
+        = number_to_human @queue_backlog, strip_insignificant_zeros: true
       .dashboard__counters__label= t 'admin.dashboard.backlog'
 
 .dashboard__widgets
diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml
index ef05a9bd6..4ab52a165 100644
--- a/app/views/admin/tags/index.html.haml
+++ b/app/views/admin/tags/index.html.haml
@@ -40,15 +40,17 @@
 
 = form_for(@form, url: batch_admin_tags_path) do |f|
   = hidden_field_tag :page, params[:page] || 1
-  = hidden_field_tag :context, params[:context]
-  = hidden_field_tag :review, params[:review]
+  = hidden_field_tag :name, params[:name] if params[:name].present?
+
+  - Admin::FilterHelper::TAGS_FILTERS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
 
   .batch-table
     .batch-table__toolbar
       %label.batch-table__toolbar__select.batch-checkbox-all
         = check_box_tag :batch_checkbox_all, nil, false
       .batch-table__toolbar__actions
-        - if params[:review] == 'pending_review'
+        - if params[:pending_review] == '1'
           = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
 
           = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
diff --git a/app/views/auth/challenges/new.html.haml b/app/views/auth/challenges/new.html.haml
new file mode 100644
index 000000000..9aef2c35d
--- /dev/null
+++ b/app/views/auth/challenges/new.html.haml
@@ -0,0 +1,15 @@
+- content_for :page_title do
+  = t('challenge.prompt')
+
+= simple_form_for @challenge, url: request.get? ? auth_challenge_path : '' do |f|
+  = f.input :return_to, as: :hidden
+
+  .field-group
+    = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off', :autofocus => true }, label: t('challenge.prompt'), required: true
+
+  .actions
+    = f.button :button, t('challenge.confirm'), type: :submit
+
+  %p.hint.subtle-hint= t('challenge.hint_html')
+
+.form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml
index b38a83d67..47112dae0 100644
--- a/app/views/auth/registrations/_status.html.haml
+++ b/app/views/auth/registrations/_status.html.haml
@@ -1,16 +1,22 @@
 %h3= t('auth.status.account_status')
 
-- if @user.account.suspended?
-  %span.negative-hint= t('user_mailer.warning.explanation.suspend')
-- elsif @user.disabled?
-  %span.negative-hint= t('user_mailer.warning.explanation.disable')
-- elsif @user.account.silenced?
-  %span.warning-hint= t('user_mailer.warning.explanation.silence')
-- elsif !@user.confirmed?
-  %span.warning-hint= t('auth.status.confirming')
-- elsif !@user.approved?
-  %span.warning-hint= t('auth.status.pending')
-- else
-  %span.positive-hint= t('auth.status.functional')
+.simple_form
+  %p.hint
+    - if @user.account.suspended?
+      %span.negative-hint= t('user_mailer.warning.explanation.suspend')
+    - elsif @user.disabled?
+      %span.negative-hint= t('user_mailer.warning.explanation.disable')
+    - elsif @user.account.silenced?
+      %span.warning-hint= t('user_mailer.warning.explanation.silence')
+    - elsif !@user.confirmed?
+      %span.warning-hint= t('auth.status.confirming')
+      = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
+    - elsif !@user.approved?
+      %span.warning-hint= t('auth.status.pending')
+    - elsif @user.account.moved_to_account_id.present?
+      %span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct)
+      = link_to t('migrations.cancel'), settings_migration_path
+    - else
+      %span.positive-hint= t('auth.status.functional')
 
 %hr.spacer/
diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml
index 710ee5c68..a155c75c9 100644
--- a/app/views/auth/registrations/edit.html.haml
+++ b/app/views/auth/registrations/edit.html.haml
@@ -13,7 +13,7 @@
       .fields-row__column.fields-group.fields-row__column-6
         = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended?
       .fields-row__column.fields-group.fields-row__column-6
-        = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?
+        = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?, hint: false
 
     .fields-row
       .fields-row__column.fields-group.fields-row__column-6
@@ -30,7 +30,18 @@
 
 = render 'sessions'
 
+%hr.spacer/
+
+%h3= t('auth.migrate_account')
+%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path)
+
+%hr.spacer/
+
+%h3= t('migrations.incoming_migrations')
+%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)
+
 - if open_deletion? && !current_account.suspended?
   %hr.spacer/
+
   %h3= t('auth.delete_account')
   %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml
index e6c3f7cca..66ed5b93f 100644
--- a/app/views/auth/shared/_links.html.haml
+++ b/app/views/auth/shared/_links.html.haml
@@ -11,7 +11,7 @@
     - if controller_name != 'passwords' && controller_name != 'registrations'
       %li= link_to t('auth.forgot_password'), new_user_password_path
 
-  - if controller_name != 'confirmations'
+  - 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'
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index a0cb7c4fe..fc690409c 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,11 +1,21 @@
 - content_for :content do
   .admin-wrapper
     .sidebar-wrapper
-      .sidebar
-        = link_to root_path do
-          = image_pack_tag 'logo.svg', class: 'logo', alt: 'Mastodon'
+      .sidebar-wrapper__inner
+        .sidebar
+          = link_to root_path do
+            = image_pack_tag 'logo.svg', class: 'logo', alt: 'Mastodon'
+
+          .sidebar__toggle
+            .sidebar__toggle__logo
+              = link_to root_path do
+                = svg_logo_full
+
+            = link_to '#', class: 'sidebar__toggle__icon' do
+              = fa_icon 'bars'
+
+          = render_navigation
 
-        = render_navigation
     .content-wrapper
       .content
         %h2= yield :page_title
@@ -14,4 +24,6 @@
 
         = yield
 
+    .sidebar-wrapper.sidebar-wrapper--empty
+
 = render template: 'layouts/application'
diff --git a/app/views/settings/aliases/index.html.haml b/app/views/settings/aliases/index.html.haml
new file mode 100644
index 000000000..5b6986368
--- /dev/null
+++ b/app/views/settings/aliases/index.html.haml
@@ -0,0 +1,29 @@
+- content_for :page_title do
+  = t('settings.aliases')
+
+= simple_form_for @alias, url: settings_aliases_path do |f|
+  = render 'shared/error_messages', object: @alias
+
+  %p.hint= t('aliases.hint_html')
+
+  %hr.spacer/
+
+  .fields-group
+    = f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }
+
+  .actions
+    = f.button :button, t('aliases.add_new'), type: :submit, class: 'button'
+
+%hr.spacer/
+
+.table-wrapper
+  %table.table.inline-table
+    %thead
+      %tr
+        %th= t('simple_form.labels.account_alias.acct')
+        %th
+    %tbody
+      - @aliases.each do |account_alias|
+        %tr
+          %td= account_alias.acct
+          %td= table_link_to 'trash', t('aliases.remove'), settings_alias_path(account_alias), data: { method: :delete }
diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml
index b13cea976..76ff76bd9 100644
--- a/app/views/settings/exports/show.html.haml
+++ b/app/views/settings/exports/show.html.haml
@@ -37,12 +37,16 @@
         %td= number_with_delimiter @export.total_domain_blocks
         %td= table_link_to 'download', t('exports.csv'), settings_exports_domain_blocks_path(format: :csv)
 
+%hr.spacer/
+
 %p.muted-hint= t('exports.archive_takeout.hint_html')
 
 - if policy(:backup).create?
   %p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post
 
 - unless @backups.empty?
+  %hr.spacer/
+
   .table-wrapper
     %table.table
       %thead
diff --git a/app/views/settings/migration/redirects/new.html.haml b/app/views/settings/migration/redirects/new.html.haml
new file mode 100644
index 000000000..017450f4b
--- /dev/null
+++ b/app/views/settings/migration/redirects/new.html.haml
@@ -0,0 +1,27 @@
+- content_for :page_title do
+  = t('settings.migrate')
+
+= simple_form_for @redirect, url: settings_migration_redirect_path do |f|
+  %p.hint= t('migrations.warning.before')
+
+  %ul.hint
+    %li.warning-hint= t('migrations.warning.redirect')
+    %li.warning-hint= t('migrations.warning.other_data')
+    %li.warning-hint= t('migrations.warning.disabled_account')
+
+  %hr.spacer/
+
+  = render 'shared/error_messages', object: @redirect
+
+  .fields-row
+    .fields-row__column.fields-group.fields-row__column-6
+      = f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }, label: t('simple_form.labels.account_migration.acct'), hint: t('simple_form.hints.account_migration.acct')
+
+    .fields-row__column.fields-group.fields-row__column-6
+      - if current_user.encrypted_password.present?
+        = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true
+      - else
+        = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true
+
+  .actions
+    = f.button :button, t('migrations.set_redirect'), type: :submit, class: 'button button--destructive'
diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml
index c69061d50..078eaebc6 100644
--- a/app/views/settings/migrations/show.html.haml
+++ b/app/views/settings/migrations/show.html.haml
@@ -1,17 +1,89 @@
 - content_for :page_title do
   = t('settings.migrate')
 
-= simple_form_for @migration, as: :migration, url: settings_migration_path, html: { method: :put } do |f|
-  - if @migration.account
-    %p.hint= t('migrations.currently_redirecting')
+.simple_form
+  - if current_account.moved_to_account.present?
+    .fields-row
+      .fields-row__column.fields-group.fields-row__column-6
+        = render 'application/card', account: current_account.moved_to_account
+      .fields-row__column.fields-group.fields-row__column-6
+        %p.hint
+          %span.positive-hint= t('migrations.redirecting_to', acct: current_account.moved_to_account.acct)
 
-    .fields-group
-      = render partial: 'application/card', locals: { account: @migration.account }
+        %p.hint= t('migrations.cancel_explanation')
+
+        %p.hint= link_to t('migrations.cancel'), settings_migration_redirect_path, data: { method: :delete }
+  - else
+    %p.hint
+      %span.positive-hint= t('migrations.not_redirecting')
+
+%hr.spacer/
+
+%h3= t('auth.migrate_account')
+
+= simple_form_for @migration, url: settings_migration_path do |f|
+  - if on_cooldown?
+    %p.hint
+      %span.warning-hint= t('migrations.on_cooldown', count: ((@cooldown.cooldown_at - Time.now.utc) / 1.day.seconds).ceil)
+  - else
+    %p.hint= t('migrations.warning.before')
+
+    %ul.hint
+      %li.warning-hint= t('migrations.warning.followers')
+      %li.warning-hint= t('migrations.warning.redirect')
+      %li.warning-hint= t('migrations.warning.other_data')
+      %li.warning-hint= t('migrations.warning.backreference_required')
+      %li.warning-hint= t('migrations.warning.cooldown')
+      %li.warning-hint= t('migrations.warning.disabled_account')
+
+  %p.hint= t('migrations.warning.only_redirect_html', path: new_settings_migration_redirect_path)
+
+  %hr.spacer/
 
   = render 'shared/error_messages', object: @migration
 
-  .fields-group
-    = f.input :acct, placeholder: t('migrations.acct')
+  .fields-row
+    .fields-row__column.fields-group.fields-row__column-6
+      = f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }, disabled: on_cooldown?
+
+    .fields-row__column.fields-group.fields-row__column-6
+      - if current_user.encrypted_password.present?
+        = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown?
+      - else
+        = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown?
 
   .actions
-    = f.button :button, t('migrations.proceed'), type: :submit, class: 'negative'
+    = f.button :button, t('migrations.proceed_with_move'), type: :submit, class: 'button button--destructive', disabled: on_cooldown?
+
+- unless @migrations.empty?
+  %hr.spacer/
+
+  %h3= t 'migrations.past_migrations'
+
+  %hr.spacer/
+
+  .table-wrapper
+    %table.table.inline-table
+      %thead
+        %tr
+          %th= t('migrations.acct')
+          %th= t('migrations.followers_count')
+          %th
+      %tbody
+        - @migrations.each do |migration|
+          %tr
+            %td
+              - if migration.target_account.present?
+                = compact_account_link_to migration.target_account
+              - else
+                = migration.acct
+
+            %td= number_with_delimiter migration.followers_count
+
+            %td
+              %time.time-ago{ datetime: migration.created_at.iso8601, title: l(migration.created_at) }= l(migration.created_at)
+
+%hr.spacer/
+
+%h3= t 'migrations.incoming_migrations'
+%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 1f62e07d8..f5d928233 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -60,6 +60,11 @@
 %h6= t('auth.migrate_account')
 %p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path)
 
+%hr.spacer/
+
+%h6= t 'migrations.incoming_migrations'
+%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path)
+
 - if open_deletion?
   %hr.spacer/
 
diff --git a/app/views/settings/two_factor_authentications/show.html.haml b/app/views/settings/two_factor_authentications/show.html.haml
index 93509e022..f1eecd000 100644
--- a/app/views/settings/two_factor_authentications/show.html.haml
+++ b/app/views/settings/two_factor_authentications/show.html.haml
@@ -2,33 +2,35 @@
   = t('settings.two_factor_authentication')
 
 - if current_user.otp_required_for_login
-  %p.positive-hint
-    = fa_icon 'check'
-    = ' '
-    = t 'two_factor_authentication.enabled'
+  %p.hint
+    %span.positive-hint
+      = fa_icon 'check'
+      = ' '
+      = t 'two_factor_authentication.enabled'
 
-  %hr/
+  %hr.spacer/
 
   = simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
-    = f.input :otp_attempt, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
+    .fields-group
+      = f.input :otp_attempt, wrapper: :with_block_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
 
     .actions
-      = f.button :button, t('two_factor_authentication.disable'), type: :submit
+      = f.button :button, t('two_factor_authentication.disable'), type: :submit, class: 'negative'
 
-  %hr/
+  %hr.spacer/
 
-  %h6= t('two_factor_authentication.recovery_codes')
-  %p.muted-hint
-    = t('two_factor_authentication.lost_recovery_codes')
-    = link_to t('two_factor_authentication.generate_recovery_codes'),
-      settings_two_factor_authentication_recovery_codes_path,
-      data: { method: :post }
+  %h3= t('two_factor_authentication.recovery_codes')
+  %p.muted-hint= t('two_factor_authentication.lost_recovery_codes')
+
+  %hr.spacer/
+
+  .simple_form
+    = link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'block-button'
 
 - else
   .simple_form
     %p.hint= t('two_factor_authentication.description_html')
 
-    = link_to t('two_factor_authentication.setup'),
-      settings_two_factor_authentication_path,
-      data: { method: :post },
-      class: 'block-button'
+    %hr.spacer/
+
+    = link_to t('two_factor_authentication.setup'), settings_two_factor_authentication_path, data: { method: :post }, class: 'block-button'
diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml
index ba34890df..d1aba6ef9 100644
--- a/app/views/statuses/_poll.html.haml
+++ b/app/views/statuses/_poll.html.haml
@@ -1,15 +1,20 @@
 - show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired?
+- own_votes = user_signed_in? ? poll.own_votes(current_account) : []
+- total_votes_count = poll.voters_count || poll.votes_count
 
 .poll
   %ul
-    - poll.loaded_options.each do |option|
+    - poll.loaded_options.each_with_index do |option, index|
       %li
         - if show_results
-          - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0
+          - percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0
           %span.poll__chart{ style: "width: #{percent}%" }
 
           %label.poll__text><
-            %span.poll__number= percent.round
+            %span.poll__number><
+              - if own_votes.include?(index)
+                %i.poll__vote__mark.fa.fa-check
+              = percent.round
             = Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
         - else
           %label.poll__text><
@@ -20,7 +25,10 @@
       %button.button.button-secondary{ disabled: true }
         = t('statuses.poll.vote')
 
-    %span= t('statuses.poll.total_votes', count: poll.votes_count)
+    - if poll.voters_count.nil?
+      %span= t('statuses.poll.total_votes', count: poll.votes_count)
+    - else
+      %span= t('statuses.poll.total_people', count: poll.voters_count)
 
     - unless poll.expires_at.nil?
       ·
diff --git a/app/views/user_mailer/two_factor_disabled.html.haml b/app/views/user_mailer/two_factor_disabled.html.haml
new file mode 100644
index 000000000..651c6f940
--- /dev/null
+++ b/app/views/user_mailer/two_factor_disabled.html.haml
@@ -0,0 +1,43 @@
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.hero
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center.padded
+                              %table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                                %tbody
+                                  %tr
+                                    %td
+                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
+
+                              %h1= t 'devise.mailer.two_factor_disabled.title'
+                              %p.lead= t 'devise.mailer.two_factor_disabled.explanation'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.content-start
+                  %table.column{ cellspacing: 0, cellpadding: 0 }
+                    %tbody
+                      %tr
+                        %td.column-cell.button-cell
+                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                            %tbody
+                              %tr
+                                %td.button-primary
+                                  = link_to edit_user_registration_url do
+                                    %span= t('settings.account_settings')
diff --git a/app/views/user_mailer/two_factor_disabled.text.erb b/app/views/user_mailer/two_factor_disabled.text.erb
new file mode 100644
index 000000000..73be1ddc2
--- /dev/null
+++ b/app/views/user_mailer/two_factor_disabled.text.erb
@@ -0,0 +1,7 @@
+<%= t 'devise.mailer.two_factor_disabled.title' %>
+
+===
+
+<%= t 'devise.mailer.two_factor_disabled.explanation' %>
+
+=> <%= edit_user_registration_url %>
diff --git a/app/views/user_mailer/two_factor_enabled.html.haml b/app/views/user_mailer/two_factor_enabled.html.haml
new file mode 100644
index 000000000..fc31bd979
--- /dev/null
+++ b/app/views/user_mailer/two_factor_enabled.html.haml
@@ -0,0 +1,43 @@
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.hero
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center.padded
+                              %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                                %tbody
+                                  %tr
+                                    %td
+                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
+
+                              %h1= t 'devise.mailer.two_factor_enabled.title'
+                              %p.lead= t 'devise.mailer.two_factor_enabled.explanation'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.content-start
+                  %table.column{ cellspacing: 0, cellpadding: 0 }
+                    %tbody
+                      %tr
+                        %td.column-cell.button-cell
+                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                            %tbody
+                              %tr
+                                %td.button-primary
+                                  = link_to edit_user_registration_url do
+                                    %span= t('settings.account_settings')
diff --git a/app/views/user_mailer/two_factor_enabled.text.erb b/app/views/user_mailer/two_factor_enabled.text.erb
new file mode 100644
index 000000000..4319dddbf
--- /dev/null
+++ b/app/views/user_mailer/two_factor_enabled.text.erb
@@ -0,0 +1,7 @@
+<%= t 'devise.mailer.two_factor_enabled.title' %>
+
+===
+
+<%= t 'devise.mailer.two_factor_enabled.explanation' %>
+
+=> <%= edit_user_registration_url %>
diff --git a/app/views/user_mailer/two_factor_recovery_codes_changed.html.haml b/app/views/user_mailer/two_factor_recovery_codes_changed.html.haml
new file mode 100644
index 000000000..833708868
--- /dev/null
+++ b/app/views/user_mailer/two_factor_recovery_codes_changed.html.haml
@@ -0,0 +1,43 @@
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.hero
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center.padded
+                              %table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                                %tbody
+                                  %tr
+                                    %td
+                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
+
+                              %h1= t 'devise.mailer.two_factor_recovery_codes_changed.title'
+                              %p.lead= t 'devise.mailer.two_factor_recovery_codes_changed.explanation'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.content-start
+                  %table.column{ cellspacing: 0, cellpadding: 0 }
+                    %tbody
+                      %tr
+                        %td.column-cell.button-cell
+                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                            %tbody
+                              %tr
+                                %td.button-primary
+                                  = link_to edit_user_registration_url do
+                                    %span= t('settings.account_settings')
diff --git a/app/views/user_mailer/two_factor_recovery_codes_changed.text.erb b/app/views/user_mailer/two_factor_recovery_codes_changed.text.erb
new file mode 100644
index 000000000..6ed12fc08
--- /dev/null
+++ b/app/views/user_mailer/two_factor_recovery_codes_changed.text.erb
@@ -0,0 +1,7 @@
+<%= t 'devise.mailer.two_factor_recovery_codes_changed.title' %>
+
+===
+
+<%= t 'devise.mailer.two_factor_recovery_codes_changed.explanation' %>
+
+=> <%= edit_user_registration_url %>
diff --git a/app/workers/activitypub/move_distribution_worker.rb b/app/workers/activitypub/move_distribution_worker.rb
new file mode 100644
index 000000000..bf1c0e7ae
--- /dev/null
+++ b/app/workers/activitypub/move_distribution_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class ActivityPub::MoveDistributionWorker
+  include Sidekiq::Worker
+  include Payloadable
+
+  sidekiq_options queue: 'push'
+
+  def perform(migration_id)
+    @migration = AccountMigration.find(migration_id)
+    @account   = @migration.account
+
+    ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
+      [signed_payload, @account.id, inbox_url]
+    end
+
+    ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url|
+      [signed_payload, @account.id, inbox_url]
+    end
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+
+  private
+
+  def inboxes
+    @inboxes ||= @migration.account.followers.inboxes
+  end
+
+  def signed_payload
+    @signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account))
+  end
+end
diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb
new file mode 100644
index 000000000..22788716f
--- /dev/null
+++ b/app/workers/move_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class MoveWorker
+  include Sidekiq::Worker
+
+  def perform(source_account_id, target_account_id)
+    @source_account = Account.find(source_account_id)
+    @target_account = Account.find(target_account_id)
+
+    if @target_account.local?
+      rewrite_follows!
+    else
+      queue_follow_unfollows!
+    end
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+
+  private
+
+  def rewrite_follows!
+    @source_account.passive_relationships
+                   .where(account: Account.local)
+                   .in_batches
+                   .update_all(target_account: @target_account)
+  end
+
+  def queue_follow_unfollows!
+    @source_account.followers.local.select(:id).find_in_batches do |accounts|
+      UnfollowFollowWorker.push_bulk(accounts.map(&:id)) { |follower_id| [follower_id, @source_account.id, @target_account.id] }
+    end
+  end
+end